diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bcad624d..204d9f6d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Config macro `YUP_EMBED_DEFAULT_THEME_TEXT_FONT` renamed to `YUP_EMBED_DEFAULT_THEME_TEXT_SERIF_FONT`; the embedded default text font now only covers the serif font. A new `YUP_EMBED_DEFAULT_THEME_TEXT_MONOSPACE_FONT` config selects whether the monospace theme font is embedded. - `SyntaxDefinition` no longer carries colors: `getColor()`, `getSelectionColor()` and the JSON `"colors"` section were removed. Token and editor colors now live in the new `CodeEditorScheme` (see `CodeEditor::setScheme`). - `Font` loading is now static-only: the instance `loadFromData()` / `loadFromFile()` methods were removed in favor of `Font::loadFontFromData()`, `Font::loadFontFromFile()`, `Font::loadFontFromFirstAvailableFile()`, `Font::loadSerifSystemTextFont()` and `Font::loadMonospaceSystemTextFont()`, all returning `ResultValue`. +- The `yup_rhi` descriptors now own their data instead of pointing at caller-managed storage. `GpuVertexBufferLayout::attributes` and `GpuPipelineOptions::vertexBuffers` are `std::vector` (dropping `attributeCount` and `vertexBufferCount`), `GpuPipelineOptions::colorTargets` is a `std::vector` capped at four (dropping `colorTargetCount`), `GpuShaderSource`'s `code` / `bindingMap` / `glFixup` are now `std::vector` (owning) instead of `Span` with `entryPoint` a `String`, and `GpuTextureDesc::label` / `GpuSamplerDesc::label` became `String`. Call sites that built a `static constexpr` attribute table to keep it alive can now build the layout inline; a `options.colorTargets[0].format = …` becomes `options.colorTargets.emplace_back().format = …`. Use `gpuShaderSourceBytes()` to build a `GpuShaderSource::code` blob from source text. `GpuDevice::beginOffscreen` now takes the new `GpuFrameDescriptor` instead of `rive::gpu::RenderContext::FrameDescriptor`. +- `GpuDevice`'s move constructor and move assignment are now `= delete`. They were publicly defaulted on a `ReferenceCountedObject`, so moving a device out from under live `GpuDevice::Ptr` holders corrupted the refcount. +- `GpuBuffer::Impl`, `GpuBuffer::getImpl()` and `GpuBuffer::createWithImpl()` moved from `public:` to `private:` (they were marked `@internal` by comment only); the backend factories that use them are friends. `GpuTexture::getOreTexture()` and `GpuTexture::getRenderImage()` were removed - neither had any caller. ### Core @@ -23,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Added a `YAML` class: a self-contained YAML parser and writer converting between YAML text and `var` (`parse`, `fromString`, `toString`, `writeToStream`, `FormatOptions`), with core-schema type resolution, block/flow collections, block scalars, and anchors/aliases/merge keys - Added a `CancelToken` class (`threads/yup_CancelToken.h`): a thread-safe, copyable observer token with `wasCancelled()`, blocking observation via `waitForCancellation()`, and callback observation via `registerCallback()`/`Registration` - Added a `CancelTokenSource` class (`threads/yup_CancelTokenSource.h`): a move-only RAII owner of a `CancelToken` that is the sole canceller, requesting cancellation automatically when destroyed (unless moved-from), with observer copies obtained via `getToken()` +- Fixed `IPAddress` disagreeing with itself about byte order inside each 16-bit group. The class reinterprets its byte array as `uint16` groups on little-endian hosts, but the string parser packed a parsed group the opposite way round, so an address parsed from `"fe80::1"` did not equal the same address built from its integer groups, and an IPv4-mapped address did not round trip through `toString()` and back or compare equal to its IPv4 counterpart. `toString()`, both `uint16` constructors, the parser and `convertIPv4MappedAddressToIPv4()` now pack and unpack a group through the same low-byte-first layout ### Audio @@ -30,6 +34,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Graphics +- `Image::getWidth()` and `Image::getHeight()` now return 0 on an invalid image instead of asserting and dereferencing null. Other accessors and pixel access still assert, as documented + - `GpuTexture` now caches the backend texture views it hands out, keyed by view descriptor. A render pass previously allocated a fresh `ore::TextureView` for every attachment on every pass and for every sampled texture on every draw - all identical frame after frame - which on Metal made building the attachment descriptors cost more than creating the command encoder they were for. The cache needs no invalidation because a `GpuTexture` wraps one underlying texture for its whole lifetime: `GpuCanvas` and `GpuTarget` build a new `GpuTexture` whenever their backing changes - `GpuRenderPass` now encodes every draw of a pass into a single backend render pass instead of opening and closing a fresh one per draw. Each `draw()` / `drawIndexed()` previously created its own command encoder - two per frame in a simple scene, one per pipeline in a multi-pass effect - which on Metal made `renderCommandEncoderWithDescriptor:` one of the most expensive calls in a frame. The pass is opened lazily by the first draw and registered with the context, so beginning another one still auto-closes it, and `GpuFrame::submit()` closes a pass the caller left open before committing. Attachments must now be bound before the first draw, which is asserted @@ -48,10 +54,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `GpuRenderPass::draw()` and `drawIndexed()` now forward `instanceCount`, `firstVertex`, `firstInstance`, `firstIndex` and `baseVertex`, so instanced drawing works - `GpuVertexStepMode::instance` was previously declarable but could never advance past instance 0. Added `setViewport()`, `setScissorRect()`, `setStencilReference()` and `setBlendColor()`, all sticky across the draws of a pass - `GpuPipelineOptions` gained `GpuColorTarget::writeMask` (`GpuColorWriteMask`), the remaining ore blend factors (`srcAlphaSaturated`, `blendColor`, `oneMinusBlendColor`) and the remaining vertex formats (`sint8x4`, `uint16x2`, `sint16x2`, `unorm16x2`, `snorm16x2`, `uint16x4`, `sint16x4`, `float16x2`, `float16x4`, `uint32`) - Fixed shader binding maps hardcoding every texture binding as a non-multisampled 2D float texture: the reflected image dimension, arrayed flag, depth flag and multisampled flag are now carried through, so a `textureCube`, `texture3D`, `texture2DArray` or depth texture no longer fails bind-group-layout validation on WebGPU. `backendSpace` is also now set to the binding's group, which was wrong for any `set != 0` +- Fixed `GpuSamplerDesc::label` dangling: `GpuSampler::create()` copied the raw `const char*` into a retained member and handed it back through the public `getDescription()`, so any caller passing a temporary got a dangling pointer for the sampler's lifetime. Both it and `GpuTextureDesc::label` are now `String` +- Fixed `GpuPipelineCache` not hashing `GpuColorTarget::writeMask`, so two pipelines differing only in write mask collided on the same cache key even though the mask is plumbed into the pipeline +- Fixed the pipeline entry-point names being borrowed rather than owned: `ore::Pipeline` keeps a shallow copy of the `PipelineDesc` and dereferences its entry-point strings at draw time, while `compileFromBundle()` pointed them at a local `String`. The compiled `GpuPipeline` now owns them, alongside the vertex layouts it already copied +- Fixed the WebGPU compute path ignoring the shader source length and requiring NUL-terminated code, which is not what an RSTB blob is - A render pass that binds a pipeline incompatible with its attachments (colour format, sample count or depth presence) now asserts and logs the backend's own diagnostic, instead of silently rendering nothing - Added a `PBR IBL` example to the graphics demo: a procedural sky baked into a float cube map face by face, convolved into an irradiance cube, prefiltered into a roughness mip chain, plus a split-sum BRDF lookup table and a CPU-uploaded albedo / normal map, shading an instanced grid of spheres against a real depth buffer - Fixed `GpuDevice::isComputeAvailable()` reporting from a runtime GL version probe on WASM / WebGL, where no GL compute implementation is compiled in at all; its documentation also claimed compute was available on D3D12 and Vulkan (neither backend exists) and unavailable on OpenGL (where it is implemented) - Fixed `native/yup_GpuDevice_dawn.cpp` guarding its whole body on `RIVE_DAWN` while the module includes it under `YUP_RIVE_USE_DAWN`, so the Dawn device could compile to nothing +- Added `GpuFrameDescriptor` (`rhi/yup_GpuTypes.h`), a field-for-field mirror of `rive::gpu::RenderContext::FrameDescriptor` that keeps `GpuDevice::beginOffscreen`'s public signature free of a Rive type. `GpuCanvas::beginDraw()` gained an optional `const GpuFrameDescriptor&` parameter (defaulting to today's behaviour: clear to transparent black, no msaa), giving callers control over `msaaSampleCount`, `ditherMode` (new `GpuDitherMode` enum), `loadOp` and `clearColor` for the offscreen 2D frame it opens - Fixed GPU compute silently stalling after a few frames on OpenGL with some drivers (AMD desktop GL): compute now runs on a dedicated, unshared GL context (`GpuDevice::Options::computeContextActivator`, routed through `GpuDevice::runOnComputeContext()`) which exclusively owns every compute resource — pipeline compilation, dispatches, storage buffer create/update/readback and deletion — falling back to the rendering context when unavailable. The GL compute pass also saves and restores the program and `GL_UNIFORM_BUFFER` bindings it touches, so it can no longer desync Rive's cached GL state - Fixed GL storage buffers being deleted right after creation (moving a `GpuBuffer::Impl` copied the plain GL buffer name, so the moved-from object's destructor freed the just-created buffer) and a crash when releasing GPU buffers or compute pipelines after their window closed (GL releases are routed through the owning device and skipped once the window's contexts are gone) @@ -97,6 +108,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - New `GpuTexture` class (`rhi/yup_GpuTexture.h`): opaque reference-counted GPU texture wrapping `rive::gpu::Texture` or `rive::gpu::RenderCanvas`. Obtained from `GpuCanvas::asTexture()` or constructed internally by `Image::fromTexture()`. - New `GpuTarget` class (`rhi/yup_GpuTarget.h`): low-level render-pass-only offscreen GPU surface (`create`, `beginRenderPass`, `asTexture`, `asImage`, `readPixels`). Its backing texture is allocated from the context's main render context, so it does not reserve a dedicated `rive::gpu::RenderContext` — use it for custom `GpuPipeline` work (e.g. post-process passes) that needs no 2D drawing. - New `GpuCanvas` class (`rhi/yup_GpuCanvas.h`): consolidated backend-agnostic offscreen GPU surface that now composes a `GpuTarget` (over a `RenderableTarget`) and creates a non-owning `Graphics` lazily only when 2D drawing is requested. +- Python bindings now expose `GpuColor` as `yup.GpuColor` (backing `GpuRenderOptions.clearColor`), comparable with `yup.Color`. +- Python bindings now expose `GpuLoadOp` / `GpuStoreOp` and `GpuRenderOptions.loadOp` / `.storeOp`. `GpuRenderOptions.clear` is kept as a bool view of `loadOp`, so `GpuRenderOptions(True, color)` and `opts.clear` still read the same as before. #### RHI module extraction & GpuDevice @@ -131,9 +144,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Lossless roundtrip tests for all formats (BMP, PNG, WebP, TGA, TIFF, PPM, GIF) now verify pixel-perfect fidelity after write→read; animated roundtrip tests for GIF, WebP, and PNG verify per-frame pixel integrity. - `StyledText::TextModifier::appendText()` gained a `Color` overload that creates (and caches per color) a solid fill paint, and `Graphics::fillFittedText()` now honors per-run style paints when every run carries one — enabling syntax-colored text. Single-color `StyledText` usage is unchanged. `Font` gained `isEmpty()`. - New `Font` static loaders: `Font::loadFontFromData()`, `Font::loadFontFromFile()`, `Font::loadFontFromFirstAvailableFile()`, `Font::loadSerifSystemTextFont()` and `Font::loadMonospaceSystemTextFont()`, all returning `ResultValue` (`wasOk()` / `failed()` / `getValue()`). The former theme-local system font lookup helpers moved into `Font`; macOS/iOS use the CoreText system UI fonts, other platforms try well-known system font files. +- `ImageFormatReader` and `ImageFormatWriter` gained a `deleteSourceWhenDestroyed` constructor parameter, defaulting to `true` so existing callers are unaffected. It lets a format be handed a stream it did not open and either take ownership of it - deleting it on destruction, as the C++ path has always done - or decline and leave the caller to own it, in which case the reader's / writer's owning pointer is released rather than deleted. The reader overload that accepts the caller's `InputStream` alongside the flag is what lets `ImageFormatManager::createReaderFor` hand a Python format the stream it opened without leaking it ### UI +- A `paint()` that throws no longer leaves the graphics frame open. `SDLComponentNative::renderFrame()` called `context->begin()` inside its render lambda but `context->end()` only after that lambda returned, so an exception from user paint code skipped the `end()`/`tick()` pair and the next frame began on a context that had never been flushed. The pair now runs from a scope guard - GUI: multitouch input on any platform with touch hardware (mobile, Emscripten in a mobile browser, and desktop touchscreens). Every finger is delivered through the existing mouse callbacks (`mouseDown` / `mouseDrag` / `mouseUp`, left button held): the first finger behaves exactly like a mouse, each additional finger arrives in parallel with a stable, dense finger index exposed as `MouseEvent::isTouch()` / `MouseEvent::getTouchIndex()`, plus touch pressure as `MouseEvent::getPressure()` (0.0-1.0). Each finger is hit-tested independently, keeps its index for the whole contact, and has its own double-click detection. SDL's synthetic touch-to-mouse events are disabled so every touch is delivered exactly once. - New "Touch Trails" example in `examples/graphics`: draws one hue-spaced colored trail per pointer (each finger gets its own color via `MouseEvent::getTouchIndex()`, with a mouse as a single pointer on desktop). Points carry an age in fade-timer ticks - not the wall clock - and their size and opacity decay with that age, so late frames can't make trails jump or flicker; the fade timer also runs while a pointer is down, dissolving the trail until only the pressed point remains, and once the finger lifts that point fades out too. - `ApplicationTheme` now exposes `setDefaultMonospaceFont()` / `getDefaultMonospaceFont()`, mirroring the existing default and icon font APIs. The default theme populates it from the embedded (or system) monospace font. @@ -263,6 +278,64 @@ The `FlexBox` and `Grid` containers landed in this cycle (they were previously l - Python bindings for `yup_ai` (`modules/yup_python/bindings/yup_YupAi_bindings.cpp`): exposes LLM client, provider, messages, tools, responses, MCP types, client, and server to Python via pybind11. +### Python Bindings (`yup_python`) + +- Bound the rest of `Component`'s public surface: `getSafeAreaBounds()`, `setPaintProfilingDisabled()` / `isPaintProfilingDisabled()`, `setMetric()` / `getMetric()` / `findMetric()`, `setCachedToTexture()` / `isCachedToTexture()`, `setComponentEffect()` / `getComponentEffect()`, `addComponentListener()` / `removeComponentListener()` and `snapshotToImage()` / `snapshotToTexture()`. The effect and listener methods needed somewhere for their argument to come from, so `ComponentEffect` (whose `apply()` is the subclass point), `ComponentListener` and `ComponentPaintMetrics` are bound too, and `addComponentListener()` keeps the Python listener alive - the C++ listener list only holds weak references. `PyComponent` also gained the `opacityChanged` override it was missing, the one `Component` virtual no Python subclass could override +- The drag-and-drop payload is bound as `yup.DragAndDropData` (`withFiles()` / `withText()` / `withUris()` builders, `getFiles()` / `getText()` / `getUris()` getters and `hasFiles()` / `hasText()` / `hasUris()` / `isEmpty()` predicates), and the five `Component` hooks that carry it - `isInterestedInDrag()`, `itemsDropped()`, `itemDragEnter()`, `itemDragMove()` and `itemDragExit()` - are callable from Python. A Python `Component` subclass could already override those hooks, but no drop could ever reach them: the payload had no Python type, so handing it back to the override threw the moment the platform delivered a drag. `withFiles()` takes any iterable of `File`, because the `Array` binding's name is derived from the typeid at runtime and is not something to require of callers +- `yup.GpuCanvas.create()` now accepts the three-argument form `create(ctx, width, height)`. pybind11 does not see C++ default arguments, so the bound signature demanded the optional `clearColor` too and every call raised `TypeError: create(): incompatible function arguments`. +- `yup.GpuCanvas.beginDraw()` returns a borrowing wrapper of the canvas's `Graphics` instead of trying to copy it. `Graphics` is non-copyable, so pybind11's default `copy` return policy made every call raise `RuntimeError: return_value_policy = copy, but type yup::Graphics is non-copyable!`; the wrapper now borrows the object and keeps the canvas alive alongside it. +- Widget style identifiers are now reachable from Python: `Label`'s nested C++ `Style` struct is bound as `yup.Label.Style`, so `label.setColor (yup.Label.Style.backgroundColorId, yup.Colors.darkblue)` works instead of raising `AttributeError: type object 'yup.Label' has no attribute 'backgroundColorId'`. The struct holds only static members and is deliberately not constructible +- `yup.Color` now converts implicitly to `yup.GpuColor`, so `yup.Colors.black` can be passed anywhere a `GpuColor` is expected (`GpuRenderOptions(True, yup.Colors.black)`) instead of raising a `TypeError`. C++ already converts implicitly - `GpuColor`'s constructor accepts any type with float-component accessors - so the binding registers that constructor and the conversion that uses it +- `Justification::Flags` members are now implicitly convertible to `Justification`, so `yup.Justification.left` can be passed straight to any API taking a `Justification` (`Graphics.fillFittedText` and friends) instead of raising a `TypeError`. Combining flags with `|` yields the underlying integer, so an `int` converts too +- An exception raised on a background thread no longer stops the dispatch loop. `paint()` runs on the render thread, and `PyErr_CheckSignals()` does nothing off the main thread, so the message-thread signal timer is the only thing that can ever notice Ctrl+C - stopping the loop from the render thread took that timer down with it and left the process deaf to SIGINT for the rest of its life. Only the message thread stops the loop now; a failing `paint()` reports and keeps running, so Ctrl+C and the window's close button still work. +- Bound `ApplicationTheme`: `yup.ApplicationTheme.getGlobalTheme()` plus `getDefaultFont()`, `getDefaultIconFont()` and `getDefaultMonospaceFont()`, so Python code can obtain theme fonts the same way C++ does. `getGlobalTheme()` raises when no theme is set instead of returning a null pointer +- Exceptions raised in Python overrides that C++ calls back into (`paint`, `timerCallback`, `messageCallback`, ...) now reach the application's `unhandledException()` with the original type and traceback, rather than terminating the process. `START_YUP_APPLICATION`'s `catchExceptionsAndContinue` now governs the no-override fallback: when true the dispatch loop absorbs the error and keeps running, when false it stops so control returns to the interpreter. Previously that path called `std::terminate()` unconditionally. +- `TestApplication` (the `yup.TestApplication` context manager used by the Python test suite) now keeps a reference to the application it constructs. The only `py::object` was a local in the scope's constructor, so the application was destroyed as soon as construction finished and `YUPApplicationBase::getInstance()` was null for the entire test. Everything guarded by it silently did nothing — most visibly `sendUnhandledException()`, so exceptions raised in Python overrides never reached `unhandledException()`. The scope now also calls `shutdownApp()` on teardown, which it never did. +- Python exception reporting no longer prints each error's location twice. Both `Helpers::printPythonException` and `PyYUPApplication::unhandledException` combined `error_already_set::what()` with `traceback.print_tb()`, but `what()` already renders the traceback itself, so every error appeared once in pybind's `At: file(line): func` form and again in Python's. Both now use `traceback.print_exception()`, which produces the single rendering the interpreter would. +- Ctrl+C is now noticed promptly instead of only when the application next happens to run Python on the main thread (which for a GUI whose only Python code is `paint()` on the render thread meant the next window focus or mouse event). The dispatch loop runs with the GIL released and CPython only runs signal handlers on the main thread, so `runApplication` now starts a message-thread timer that calls `PyErr_CheckSignals()` every `messageManagerGranularityMilliseconds` — a parameter that was passed but never used. It is a timer rather than a sliced `runDispatchLoopUntil()` because on Apple platforms that overload does not invoke the event loop callback that pumps SDL. +- Ctrl+C stops a Python application again. Now that the dispatch loops catch rather than letting the `KeyboardInterrupt` unwind out of `runDispatchLoop()`, `unhandledException` has to stop the loop itself — it previously only set `caughtKeyboardInterrupt` and returned, leaving the loop running. `runApplication` also no longer re-raises a signal it has already reported. +- Exception reporting no longer raises from inside the handler when the traceback is null, which is the case for a `KeyboardInterrupt` delivered by `PyErr_CheckSignals()` at a C boundary — a null `py::object` cannot be passed to a Python call and threw `cast_error`. +- `PyYUPApplication::unhandledException` no longer dereferences a null `std::exception*`. `sendUnhandledException()` passes null for a `catch (...)`, which the newly added `YUP_CATCH_EXCEPTION` sites make reachable. +- Destroying a Python `Component` that is on the desktop no longer deadlocks the process. Tearing the native peer down joins the render thread, which can be blocked acquiring the GIL inside `paint()` / `refreshDisplay()` - and Python holds the GIL while it destroys the component, so neither thread could proceed and the message thread wedged inside the SDL event pump, taking the window's close button, the dock icon and Ctrl+C with it. The `PyComponent` trampoline destructor now detaches from the desktop with the GIL released, and `addToDesktop()` / `removeFromDesktop()` are bound with a `gil_scoped_release` call guard. +- `unhandledException` reporting no longer touches Python from the thread that threw. Reporting from the render thread meant acquiring the GIL with no abort path while the message thread could be holding it to join that same thread, and after an exception it ran once per frame - `import traceback`, format, print - which is what made the deadlock above reproducible. Off the message thread the reporting and the stop request are now marshalled to it as a single message, so a stop can never latch the loop shut before the report is dispatched, and the throwing thread touches no Python at all. `reportUnhandledException` is also `noexcept` throughout: it is always called from a `YUP_CATCH_EXCEPTION` handler running in a C or Objective-C frame, where a raising `unhandledException` override would previously escape and leave the platform event locks permanently held. +- An exception on the render thread now stops the application instead of repeating forever. `unhandledException` returned early on any thread that was not the message thread, so with `catchExceptionsAndContinue=False` a failing `paint()` was reported on every frame and the app never quit; `stopDispatchLoop()` is safe to call from any thread and is now used. +- Ctrl+C no longer throws across the platform timer's C frame. The signal-check timer captured the raised error and stops the loop in place; `START_YUP_APPLICATION` re-raises it as a normal `KeyboardInterrupt` after the application has shut down. `START_YUP_APPLICATION` also always shuts the application down now - previously a re-raised exception or a signal checked after the loop skipped `shutdownApp()` entirely. +- `PyYUPApplication::unhandledException` imported `__builtins__`, which is not a module in `sys.modules`, so the branch handling a non-Python exception raised from inside the exception handler. It now uses `PYBIND11_BUILTINS_MODULE`. + +- The `yup_rhi` bindings now cover the surface the RHI grew: textures (`GpuTexture.create` / `upload`, `GpuTextureDesc`, `GpuTextureViewDesc`, `GpuTextureDataDesc`), samplers (`GpuSampler`, `GpuSamplerDesc`, `GpuRenderPass.setSampler`), the full render-pass API (`setColorAttachment`, `setDepthStencilAttachment`, `setResolveTarget`, `setViewport`, `setScissorRect`, `setStencilReference`, `setBlendColor`, `GpuDepthStencilOptions`), vertex layouts and multi-target pipeline options, compute (`GpuComputePipeline`, `GpuComputePass`, `GpuWorkgroupSize`), the `GpuTarget` texture and view overloads with `readPixels()`, and the `GpuDevice` capability probes and buffer create/update/read entry points +- Fixed `GpuPipeline.compile*` being uncallable from Python: they return `ResultValue`, which has no registered Python type, so every call raised at return-conversion time. They now unwrap and raise the compiler diagnostics as a Python exception. `GpuTarget.beginRenderPass` was not bound at all, and `GraphicsContext` had no class binding, so `Graphics.getGraphicsContext()` raised as well - between them, neither `python/demos/gpu_triangle.py` nor `gpu_effects.py` could run +- `GpuRenderPass.setUniformBuffer` and the buffer entry points take any buffer-protocol object (`bytes`, `bytearray`, `memoryview`, numpy arrays) instead of only `bytes`, and size them in bytes rather than items +- `GpuTextureFormat` exposes all 25 formats (5 were bound), and `GpuVertexFormat` all 17 (7 were bound). Added `GpuColorWriteMask` (composable with `|` and `&`), `GpuFilter`, `GpuWrapMode`, `GpuTextureType`, `GpuTextureAspect`, `GpuTextureViewDimension`, `GpuBufferType.storage` and the remaining blend factors +- `GpuFrame` and `GpuRenderPass` keep the objects they borrow alive (`py::keep_alive`), so a pass can no longer outlive the frame whose pools it points into +- `GpuShaderSource` is now exposed, now that its blob fields own their data: `code`, `bindingMap` and `glFixup` are bytes-in, bytes-out properties accepting any buffer-protocol object. `GpuPipeline.compile` and `GpuComputePipeline.compile` are bound alongside the existing `compileFromGlsl`, so Python can compile from native shader sources without going through the GLSL transpiler. `GpuCanvas.beginDraw()` gained an optional `GpuFrameDescriptor` argument (also newly bound, along with `GpuDitherMode`), giving Python control over msaa/dither/loadOp/clearColor for the offscreen 2D frame it opens - bound as two overloads rather than one defaulted argument, since `registerYupGraphicsBindings` runs before `registerYupRhiBindings` and a default value referencing `GpuFrameDescriptor` at bind time would throw at import +- Fixed `registerYupRhiBindings` being called under `YUP_MODULE_AVAILABLE_yup_graphics` while its translation unit compiles under `YUP_MODULE_AVAILABLE_yup_rhi`, which silently dropped the RHI bindings in a graphics-less build +- Added `python/demos/gpu_cube.py`: a textured, depth-tested spinning cube driving vertex and index buffers, an uploaded texture and a sampler entirely from Python +- `python/demos/gpu_cube.py` no longer renders its cube inside out. Its face corners wound counter-clockwise seen from outside, but the RHI bakes a clip-space Y-flip into the vertex stage — the GL backend inverts `glFrontFace` and Vulkan relies on naga's `ADJUST_COORDINATE_SPACE` to compensate for it — so `GpuCullMode.back` culled the side facing the camera instead of the far side. The corner order now matches `SpinningCubeDemo`'s `kCubeVerts` +- Fixed windows never appearing when a YUP application is run from a Python interpreter on macOS. A bare interpreter is not a bundled app, so the process starts as a non-UI one; SDL would normally set the activation policy, but it only does so when it is the one to create `NSApp`, and YUP's `MessageManager` gets there first. `START_YUP_APPLICATION` now transforms the process to a foreground application up front. Deliberately not done in `initialiseYup_Windowing()`, so a plugin hosted in a DAW can never transform its host's process +- Added `ColorGradient` bindings (plus its `Type` / `Spread` enums and nested `ColorStop`). The previous binding had been commented out when the C++ API replaced `bool isRadial` with a `Type` enum, which left `Graphics.setFillColorGradient()` / `setStrokeColorGradient()` bound but uncallable +- `Component::paintSubtree` now clears its `isRepainting` flag through a scope guard. A `paint()` override that throws - which is how a Python error surfaces - previously skipped the reset, leaving the flag set permanently so every later `repaint()` of that component tripped an assertion pointing at the wrong cause +- Bound the `yup_core` facilities that had no Python surface at all: `Logger` (with `setCurrentLogger()` accepting a Python logger object), `FileLogger`, `DynamicLibrary`, `SHA1`, `CancelToken` / `CancelTokenSource`, `WaitableTimer`, `StringPool`, `TextDiff`, `DynamicObject`, `AbstractFifo` / `SingleThreadedAbstractFifo`, `Expression` (plus its `ExpressionScope`), `LocalisedStrings`, `YAML` (plus `FormatOptions` / `Spacing`), `IPAddress`, `MACAddress`, `NamedPipe`, `WebInputStream` and the `InputSource` family +- `registerStatisticsAccumulator()` lets a C++ statistics type be addressed from Python the way the other generic templates are, which is what makes `yup.StatisticsAccumulator[float]` resolve instead of raising. Only `float` is registered: Python has a single float type, so registering `double` as well would map both spellings onto the same key and overwrite the first rather than adding an overload +- Bound `Fitting`, `CubicBezier` and `Drawable` in `yup_graphics`; `KeyModifiers`, `KeyPress`, `MouseWheelData`, `ProgressBar` and `SwitchButton` in `yup_gui`; and `MessageBase`, `Message`, `CallbackMessage` and `MessageListener` in `yup_events`. The two widgets needed trampolines for the same reason the rest do - `paint()` is the subclass point +- Fixed `ImageFormatManager::createReaderFor` leaking the stream it opened. The manager opens the file and hands the reader an `InputStream` to own, but `createReaderFor` released the Python wrapper of that stream on the way in while the format it built had no way to adopt it, so every call leaked one `FileInputStream`. `ImageFormatReader` now has a constructor that takes the caller's stream with `deleteSourceWhenDestroyed` set, and the bindings hand a Python format the stream the manager opened, so the reader deletes it exactly as the C++ path does +- Methods that take a `std::unique_ptr` parameter are only callable with a Python-constructed `T` when `T` is registered with `py::smart_holder`: pybind11 3.x moves the object out of the wrapper and disowns it, which it cannot do for a `unique_ptr` holder. `InputSource` (with `FileInputSource` and `URLInputSource`), `XmlElement` and the whole `InputStream` hierarchy (`FileInputStream`, `MemoryInputStream`, `BufferedInputStream`, `SubregionStream`, `GZIPDecompressorInputStream`, `WebInputStream`) were migrated, so `XmlDocument.setInputSource()`, `XmlElement.addChildElement()` and `ZipFile.Builder.addEntry()` now transfer ownership instead of releasing a wrapper nothing accounted for. The trampolines for those hierarchies also carry `trampoline_self_life_support`, which is a `smart_holder` requirement - pybind11 rejects the combination at compile time otherwise +- `MessageBase`, `Message` and `CallbackMessage` are registered on their natural `ReferenceCountedObjectPtr` holder instead of the default `unique_ptr`. `MessageBase::post()` and `MessageListener::postMessage()` manage the message themselves - the queue takes its own reference, and the failure path only deletes a message whose count is still zero - so the previous bindings had to hand ownership over through `release()` to stop Python deleting a message the queue still held. With the refcounted holder both methods bind directly, and a message Python still holds stays valid after posting +- These methods now consume the Python object they are given: after `addChildElement()`, `setInputSource()`, `addEntry()` or `ZipFile(stream)`, using the wrapper raises `ValueError: ... Python instance was disowned`. That is the ownership the C++ API documents - the container deletes what it was given - and it replaces the previous behaviour, which leaked the reference instead +- The new surface is covered by additions to `python/tests/`, and `docs/scripting/python-bindings-coverage.md` records the per-module inventory of bound against declared API that these gaps were found from +- Bound `MouseEvent`, `MouseListener` and `TextInputTarget`, and declared the `MouseListener` base of `Component`. No Python `Component` subclass could receive a mouse callback that carried an event: `mouseMove()`, `mouseDrag()`, `mouseUp()`, `mouseDoubleClick()` and `mouseWheel()` were already routed to overrides, but `yup.MouseEvent` did not exist, so the moment the platform delivered one, the conversion back to Python threw. `Component::addMouseListener()` now also keeps the Python listener alive, matching `addComponentListener()`, and the wheel trampoline looks its override up under the C++ name `mouseWheel` - it asked for `mouseWheelMove`, a name nothing in the tree defined, so a wheel override written the obvious way was silently never called +- Bound the `yup_gui` input and widget types the coverage page still listed as missing: `ScrollBar`, `ListBox`, `ListBoxModel`, `ListBoxItem`, `ComboBox` and `TextEditor`, each with its nested enums (`ScrollBar::Orientation`/`VisibilityMode`, `ListBox::Orientation`/`SelectionMode`, `ListBoxItem::IconPosition`) and its nested `Style` struct exposed for its theme identifiers the way `yup.Label.Style` already was. `ListBoxModel` is the subclass point for a Python list model and is dispatched through a trampoline; `setModel()` pins the model, which the ListBox never owns. `TextEditor` needed its own trampoline for `getTextInputRect()`, the `TextInputTarget` virtual it implements +- `ListBoxItem::setIconDrawable()`/`getIconDrawable()` and `ListBoxModel::refreshComponentForRow()` are deliberately not bound: the first pair traffic in `std::shared_ptr` while `Drawable` uses pybind11's default `unique_ptr` holder, and the second hands the `ListBox` ownership of the component it returns, which cannot be taken away from a Python-owned instance without inviting a double free. `setIcon()` and `paintListBoxItem()`/`getRowText()`/`getRowIcon()` cover the same ground; both exclusions are recorded on the coverage page +- Bound `AudioIODeviceType` and `AudioDeviceManager::getAvailableDeviceTypes()`, which `python/demos/audio_device.py` needs to list what the machine offers — the demo raised `AttributeError: 'yup.AudioDeviceManager' object has no attribute 'getAvailableDeviceTypes'`. The manager owns its device types, so `getAvailableDeviceTypes()` returns a fresh Python list whose entries borrow from it: a caller keeping a type keeps the manager alive alongside it, and `createDevice()` hands Python the device it creates with `take_ownership`, which is what the C++ contract asks for. `AudioIODeviceType::Listener`, `addListener()` and `removeListener()` stay unbound for want of a trampoline +- `PositionableAudioSource` was bound without declaring its `AudioSource` base, so pybind11 treated the two as unrelated types: `python/demos/audio_player.py` died with `TypeError: setSource(): incompatible function arguments ... (self: yup.AudioSourcePlayer, newSource: yup.AudioSource) ... Invoked with: ..., `. An audit of all 289 `py::class_` registrations against the module headers found this to be the only such omission (a scan that has to match the two-phase `py::class_<...> classX (m, "X")` form, since the single-phase declarations are all correct). `AudioSourcePlayer::setSource()` and `AudioTransportSource::setSource()` now also pin the source they are given with `py::keep_alive`: both C++ contracts say the object playing it does not own it, and nothing in Python could otherwise express that +- `AudioFormatReaderSource` no longer takes `deleteReaderWhenThisIsDeleted` from Python, and no longer reads a freed reader. `createReaderFor()` hands Python a `std::unique_ptr`, and pybind11 cannot take a raw-pointer argument's ownership away from a live wrapper, so `AudioFormatReaderSource(reader, True)` — what `python/demos/audio_player.py` and `audio_player_waveform.py` both did — built a source that deleted a reader Python was about to delete too, and that read the reader *after* Python had dropped it. The source now always borrows and is pinned to the reader with `py::keep_alive`, so the reader outlives every use of it (C++ callers wanting the transfer still use the `std::unique_ptr` constructor). The symptom this fixes is not a crash: a dead reader reports a total length of 0, `AudioTransportSource::hasStreamFinished()` compares that with the read position, `0 >= 0`, and `getNextAudioBlock()` clears the transport's `playing` flag on the first block, so the transport went silent and `isPlaying()` never became true +- `yup.AudioBuffer` is subscriptable, so `yup.AudioBuffer[float]` resolves to `yup.AudioBufferFloat` the way `yup.Rectangle[float]` and `yup.StatisticsAccumulator[float]` already did — `python/demos/audio_player_waveform.py` failed with `TypeError: type 'yup.AudioBufferFloat' is not subscriptable`. The existing callable alias keeps working, so a subscription was added to it rather than replacing it with the type-keyed dictionary the other templated types expose: Python has one floating-point type, so such a dictionary could hold `float` alone and the double specialization stays reachable as `AudioBufferDouble` +- `python/demos/audio_player_waveform.py` no longer glitches while it plays. Its `paint()` read the whole file through the same `AudioFormatReader` the audio thread was pulling, about 800 reads per frame: `AudioFormatReader::read()` re-seeks its stream and allocates on every call and the reader takes no lock, so the two threads moved the shared stream position out from under each other, and the audio thread competed for the allocator with a storm of render-thread reads. The peak envelope is now computed once, before playback starts, and painting only reads that list +- `python/demos/layout_flexgrid.py` lays its panels out again. Every `FlexItem` was built with an explicit width of 0, and a literal 0 is a real zero size rather than "auto", so `align-items: stretch` skipped all five labels and the window showed nothing but the component's own black background. The sidebars were also added to a nested `bodyFlex` that never had `performLayout()` called on it, and `self.content` was added to both boxes - the nested row now runs over the band the column box leaves for the content, in a `Rectangle[float]` since `Component::getLocalBounds()` returns floats and `RectangleInt` rejects them +- `python/demos/layout_rectangles.py` runs at all now. Its `paint()` had never executed past its first statement: `w - 80` measured from float component bounds was fed to `Rectangle[int]`, the eight `.to()` conversions are the C++ spelling of the Python `toFloat()`, `Graphics::drawText` does not exist (`fillFittedText (text, font, rect, justification)` is the API, and the font comes from `ApplicationTheme`), `Justification::centred` became `Justification::center`, and the greys are `darkgray`/`gray`/`lightgray` - JUCE's British spellings were not carried over. It also carves its frame out of the window bounds with `removeFrom*` instead of subtracting from `w` and `h`: the subtraction went negative once the window was narrower than the 40px margin, and `removeFrom*` asserts on a negative extent (`jlimit (0, extent, delta)` in `yup_Rectangle.h`), so resizing the window down to zero width tripped it +- Note for anyone else driving widgets from Python: laying widget text out goes through `ApplicationTheme` (`ComboBox::updateDisplayText()`, `TextEditor`'s styled text and `ListBoxItem::calculateLayout()` all ask the theme for a font), and the global theme only exists while an application is initialised. Outside one those lookups dereference a null `ReferenceCountedObjectPtr` and take the process down, so a widget that carries text has to be created and used inside a running application - in the suite that is the `juce_app` fixture `test_ApplicationTheme.py` already used, which the new widget tests take as well + +- `python/demos/matplotlib_integration.py` is an actual port of popsicle's demo now, instead of a chart drawn out of YUP primitives. `make_plot()` / `generate_plot_png()` build the linear-regression figure in a child process - matplotlib is not thread safe, so it stays off the UI thread - and hand the PNG back over a `multiprocessing.Queue`; a 24Hz `yup.Timer` polls that queue, decodes the bytes with `Image.loadFromData()` into a child component that paints them, and fades that child in with `setOpacity()` while a star spinner turns behind it. Four YUP-for-JUCE substitutions were needed: the chart widget is a plain `Component` (YUP has no `DrawableImage`), the fade is driven from the timer (no `Desktop::getAnimator()`), `fillAll()` takes no color so the white fill is `setFillColor()` + `fillAll()`, and the child is added with `addAndMakeVisible()` because a YUP component starts out with `isVisible() == false`, where JUCE's starts visible. Two of those were only found by running it. The image child must not be opaque: `Component::hasOpaqueChildCoveringArea()` ignores the child's opacity, so an opaque child covering the parent makes `internalPaint()` skip the parent's `paint()` entirely and the window showed nothing but black - no white background, no spinner - until the chart arrived. And the timer is a plain `ChartPoller(yup.Timer)` holding the component rather than a second base of it, because a Python class deriving from two bound YUP classes (`Component` + `Timer`, as the original's `MainContentComponent(juce.Component, juce.Timer)` is) crashed with a bad `this` inside `PyComponent`'s trampoline destructor when the window closed - the dealloc walk of such an instance runs pybind11's multiple-inheritance value_and_holder bookkeeping, and the class had been registered without the pair the pybind11 documentation asks of every trampoline. The Gui trampolines now derive from `pybind11::trampoline_self_life_support` and their classes are registered with `py::smart_holder`, which is what the Core and Graphics bindings had already been doing for their own trampolines. `python/tests/test_yup_gui/test_MultipleInheritance.py` covers it: the two-base case runs in a child interpreter, because the failure mode is a signal rather than an exception, and the test fails if the child is killed by one instead of reporting an error. `Image.loadFromData()` raises `ValueError` on a payload it cannot decode, where the `ImageCache::getFromMemory()` it replaces returned a null `Image` + ### Examples - `SpinningCubeDemo` example (`examples/graphics`): rewritten to the new RHI shape — `GpuFrame` + `GpuCanvas::beginDraw` + `GpuRenderPass` for both the indexed cube draw and the separable two-pass blur (H+V sharing one `GpuFrame`), `isGpuAvailable()` capability probe, and live GLSL editing via `GpuPipeline::compileFromGlsl`. The default Lottie animation is now played back per-frame into an offscreen `GpuCanvas` (2D path) and sampled by the cube's fragment shader so the animation is texture-mapped onto every cube face. @@ -280,12 +353,16 @@ The `FlexBox` and `Grid` containers landed in this cycle (they were previously l ### Testing +- The AU and AUv3 wrapper tests no longer describe a stereo buffer list with a stack-allocated `AudioBufferList`. Only the first `AudioBuffer` is reserved inside the struct, so `AUStateTests.RenderProducesOutput` and the two `AUv3BypassRenderTests` render tests wrote past the object while filling `mBuffers[1].mDataByteSize`, aborting the suite under AddressSanitizer with a stack-buffer-overflow. All three now build their lists with a new `tests/yup_audio_plugin_client/yup_TestAudioBufferList.h` helper, which owns an allocation sized for the number of buffers asked for - The `yup_events` Python tests no longer depend on a single fixed-duration pump of the message loop. `next(juce_app)` runs the dispatch loop for 20ms and returns, but a dispatched callback still has to re-acquire the GIL before the Python side runs, so on a loaded machine it can land after the pump has already returned - `test_MessageListener::test_construct_and_post` failed this way on CI. The 22 call sites with a positive expectation now use a new `pump_until(app, predicate)` helper in `python/tests/utilities.py`, which pumps in short slices until the condition holds or a 5s timeout expires. The four sites that assert a *negative* after pumping ("still zero because it was cancelled") deliberately keep the fixed pump, since polling a negative predicate returns immediately and proves nothing - `Component` now befriends a single `ComponentTestHelper` class template instead of accumulating one friend class per test suite; unit tests specialize it (e.g. `ComponentTestHelper`, `ComponentTestHelper`) to reach private state. - Nine test files were globbed into the IDE project but never `#include`d in their module's unity translation unit, so 141 tests had never been compiled or run since being written: `yup_CodeEditorScheme.cpp`, `yup_ListBoxItem.cpp` and `yup_PaintProfileStats.cpp` (yup_gui), `yup_Memory.cpp` and `yup_TypeErasedObject.cpp` (yup_core), `yup_GraphicsContext.cpp` and `yup_ImageFormatMetadataExtended.cpp` (yup_graphics), `yup_AudioDeviceManagerWindow.cpp` (yup_audio_gui) and `yup_AudioPluginLV2Format.cpp` (yup_audio_plugin_host). All are now wired up, which took three kinds of repair: two defined a file-scope helper that a sibling in the same unity build already defined (`makeSample`, `loadFromBlock`), so the newly enabled copies are renamed rather than the working ones; `yup_ListBoxItem.cpp` had drifted against the graphics API (`PixelFormat` is no longer nested in `Image` and has no `ARGB`, the `Image` constructor now takes `(w, h, format)`, `DrawablePath` folded into `Drawable`, and `String` has no `(count, char)` constructor); and `yup_AudioPluginLV2Format.cpp` is now wrapped in `#if YUP_AUDIO_PLUGIN_HOST_ENABLE_LV2`, mirroring the guard the module puts around `LV2Format`, since the test target does not enable LV2. Wiring them up surfaced two genuine defects the dead tests had been written against: the PNG raw-chunk writer bug below, and `ListBoxItem::setIcon` being an unimplemented stub - the four tests depending on it are marked `DISABLED_` with a pointer to the TODO rather than weakened to match the stub ### Bug Fixes +- AUv2 wrapper: an input bus that is not fed during a render cycle is now presented to the processor as a null-channel view. `buildInputBusViews` filled the per-bus channel pointers only for the channels it actually received, leaving the rest at whatever the previous render had stored there, so a sidechain input the host stopped feeding (inactive element or a failed `PullInput`) kept pointing at that element's stale audio instead of reading as silent - contradicting the comment on `pullAuxiliaryInputElements` and the `AudioBusBufferView` "null for an inactive or silent bus" contract. The input path now clears each bus's slots first, exactly as `buildOutputBusViews` already did for outputs; the AUv3 wrapper never had the problem because it maps input views onto its own scratch buffers +- `MessageManager` on Apple platforms: `runDispatchLoop()` and `runDispatchLoopUntil()` now wrap their loop body in `YUP_TRY`/`YUP_CATCH_EXCEPTION`, matching the generic implementations in `yup_MessageManager.cpp` that `#if ! (YUP_MAC || YUP_IOS || YUP_WASM)` compiles out on these platforms. The `.mm` replacements previously caught only `NSException`, a disjoint set from `std::exception`, so a C++ exception thrown by a message callback escaped the dispatch loop instead of reaching `YUPApplicationBase::sendUnhandledException()` — which made `unhandledException()` unreachable on macOS and iOS, and killed the application on the first failure. +- The SDL render thread now routes exceptions from `renderFrame()` through `YUP_CATCH_EXCEPTION` as well. It never passes through a dispatch loop, so an exception escaping `paint()` reached `Thread::threadEntryPoint()`, which only asserts — rendering then stopped permanently with no diagnostic in release builds. - PNG: `png/iCCP` and `png/cHRM` raw chunks are now actually written. The iCCP branch in the writer was an empty `if` body with a "for simplicity, write as unknown chunk" comment that never wrote anything, and `png/iCCP` was excluded from the unknown-chunk loop; cHRM was collected but silently dropped by libpng, because on write libpng only emits unknown chunks whose name marks them safe-to-copy (a lowercase fourth letter) unless `png_set_keep_unknown_chunks` says otherwise - `eXIf` is safe-to-copy and survived, `cHRM` and `iCCP` are not and did not. Both are now registered with `PNG_HANDLE_CHUNK_ALWAYS`. The `png_unknown_chunk` is also value-initialised, since libpng copies all five name bytes and the terminator was left indeterminate - `FlexBox`: the `gap` is no longer applied after the last item on each line. It was added unconditionally after every item, so when items grew or shrank to fill the container exactly (e.g. `flexGrow` items with `gap`), the trailing gap pushed the final item past the container's main-axis edge and it got clipped (e.g. the last panel in each row of the `Layout` example). - `ShaderTranspiler`: GLSL ES fragment output now defaults to `precision highp float;` instead of SPIRV-Cross's `precision mediump float;` default. Desktop GLSL implies highp and glslang records no precision decorations for it, so SPIRV-Cross could only re-qualify declared variables; anything left to the fragment default — uniform block members and inlined expression intermediates — silently ran at mediump on OpenGL ES, corrupting fp32-exact math such as the fixed-point field codecs of the GPU fluid simulation demo (pixelated dye that never fades on Android). The ESSL default is now highp in both the emit and reflect paths. @@ -325,6 +402,7 @@ The `FlexBox` and `Grid` containers landed in this cycle (they were previously l - Windows toasts emit the `scenario` attribute with the spellings the toast schema declares (`reminder` / `alarm` / `incomingCall`) rather than the capitalised WinToast ones, which are not part of the enumeration. Schema conformance only — it is not the cause of the toasts that fail to display on Windows 11, see `docs/Windows Toast 80070490 Analysis.md` - Windows toasts report a real permission state instead of always claiming `granted`: `ToastNotification::getPermissionState()` / `requestPermission()` now query `IToastNotifier::get_Setting()`, so an application, user, group policy or manifest level block is visible to the caller. The setting is also logged next to the payload. Note that it does not cover Do Not Disturb or the per-app "show notification banners" switch, which suppress the on-screen banner while still delivering the toast to the notification center - Windows toasts no longer hand `put_ExpirationTime` a stack object that dies at the end of the enclosing `if` block. The notification retains that `IReference` for its whole life, so it was already dangling by the time `Show()` read it; it is now a reference-counted `ComBaseClassHelper` that the notification keeps alive +- `TypeErasedObject` now relocates its payload through the payload's move constructor instead of a byte copy. Moving a payload that points into itself - libstdc++'s small-string `std::string`, a `std::map` header, or anything caching a member's address - left those pointers aimed at the dead source buffer, so `GpuPipeline::Impl`'s entry-point strings freed a stale stack address when the pipeline was destroyed. glibc reported it as `free(): invalid pointer` and killed `yup_tests` in `GpuAttachmentMockTests` on Linux, while libc++'s `std::string` has no self-pointer, so macOS never saw it ### Documentation diff --git a/docs/graphics/rhi/compute-shaders.md b/docs/graphics/rhi/compute-shaders.md index cd1698845..4b554fef3 100644 --- a/docs/graphics/rhi/compute-shaders.md +++ b/docs/graphics/rhi/compute-shaders.md @@ -67,8 +67,7 @@ if (bundle.wasOk()) ```cpp GpuShaderSource source; source.language = GpuShaderLanguage::msl; // or hlsl, wgsl, glsl -source.code = mslSource; -source.codeSize = mslLength; +source.code = gpuShaderSourceBytes (mslSource); // must outlive the compile call auto result = GpuComputePipeline::compile (device, source, { 256, 1, 1 }); ``` diff --git a/docs/graphics/rhi/pipelines.md b/docs/graphics/rhi/pipelines.md index 582d53ff8..1730092e8 100644 --- a/docs/graphics/rhi/pipelines.md +++ b/docs/graphics/rhi/pipelines.md @@ -84,22 +84,31 @@ Describes one pipeline stage's compiled source plus its mandatory metadata: struct GpuShaderSource { GpuShaderLanguage language = GpuShaderLanguage::wgsl; // wgsl | glsl | msl | hlsl - const void* code = nullptr; // source/bytecode - uint32_t codeSize = 0; - - const uint8_t* bindingMap = nullptr; // mandatory RSTB sidecar - uint32_t bindingMapSize = 0; - - const uint8_t* glFixup = nullptr; // GL-only name→slot table - uint32_t glFixupSize = 0; - - const char* entryPoint = nullptr; // null → "vs_main" / "fs_main" + Span code; // source/bytecode + Span bindingMap; // mandatory RSTB sidecar + Span glFixup; // GL-only name→slot table + String entryPoint; // empty → "vs_main" / "fs_main" }; ``` `GpuShaderLanguage` values: `wgsl` (WebGPU), `glsl` (GLES 3.0+, GL path only), `msl` (Metal only), `hlsl` (Direct3D only). +The three blob fields **own their data** (`std::vector`), so a descriptor +built from temporaries stays valid for as long as the descriptor does. +`gpuShaderSourceBytes()` builds one from source text: + +```cpp +GpuShaderSource vs; +vs.language = GpuShaderLanguage::glsl; +vs.code = gpuShaderSourceBytes (vertexSource); // const char* or String +vs.bindingMap = bindingMapBlob; // std::vector +``` + +`GpuShaderSource` is exposed to Python too, with `code` / `bindingMap` / +`glFixup` as bytes-in, bytes-out properties; see +[GPU rendering from Python](../../scripting/python-rhi.md). + ### Binding maps `GpuPipeline` (and the underlying GPU layer) require a pre-compiled **RSTB @@ -156,16 +165,14 @@ post-process pipeline: no vertex buffers, no culling, a single alpha-blended ```cpp struct GpuPipelineOptions { - const GpuVertexBufferLayout* vertexBuffers = nullptr; // null for fullscreen - uint32_t vertexBufferCount = 0; + std::vector vertexBuffers; // empty for fullscreen GpuPrimitiveTopology topology = GpuPrimitiveTopology::triangleList; GpuIndexFormat indexFormat = GpuIndexFormat::none; GpuCullMode cullMode = GpuCullMode::none; GpuFaceWinding winding = GpuFaceWinding::counterClockwise; - GpuColorTarget colorTargets[4] = {}; // up to 4; count 0 → one default target - uint32_t colorTargetCount = 0; + std::vector colorTargets; // up to 4; empty → one default target GpuDepthStencilState depthStencil; // enabled = false by default GpuStencilFaceState stencilFront, stencilBack; @@ -176,31 +183,30 @@ struct GpuPipelineOptions }; ``` +`vertexBuffers` and `colorTargets` own their contents, and so does +`GpuVertexBufferLayout::attributes`. Building a descriptor from temporaries is +therefore safe - no `static constexpr` table has to be kept alive alongside it. + ### Custom geometry example For 3D or custom 2D geometry, describe the vertex layout, enable culling, and (optionally) depth testing: ```cpp -const GpuVertexAttribute attribs[] = { - { GpuVertexFormat::float3, 0, 0 }, // position @location(0) - { GpuVertexFormat::float4, sizeof (float) * 3, 1 }, // color @location(1) - { GpuVertexFormat::float3, sizeof (float) * 7, 2 }, // normal @location(2) -}; +GpuPipelineOptions options; -const GpuVertexBufferLayout layout { +options.vertexBuffers.emplace_back ( sizeof (float) * 10, // stride GpuVertexStepMode::vertex, - attribs, - (uint32_t) std::size (attribs) -}; - -GpuPipelineOptions options; -options.vertexBuffers = &layout; -options.vertexBufferCount = 1; -options.indexFormat = GpuIndexFormat::uint16; -options.cullMode = GpuCullMode::back; -options.winding = GpuFaceWinding::counterClockwise; + std::vector { + { GpuVertexFormat::float3, 0, 0 }, // position @location(0) + { GpuVertexFormat::float4, sizeof (float) * 3, 1 }, // color @location(1) + { GpuVertexFormat::float3, sizeof (float) * 7, 2 }, // normal @location(2) + }); + +options.indexFormat = GpuIndexFormat::uint16; +options.cullMode = GpuCullMode::back; +options.winding = GpuFaceWinding::counterClockwise; options.depthStencil.enabled = true; ``` @@ -230,7 +236,7 @@ The pipeline configuration draws on a family of small enums, all mirroring the - **`GpuTextureFormat`** - the full format set, documented under [Buffers & Textures](buffers-and-textures.md#gputexture). -When `colorTargetCount` is greater than one, the pass must bind a matching +When `colorTargets` holds more than one entry, the pass must bind a matching attachment for each target with `GpuRenderPass::setColorAttachment()`, and each `colorTargets[i].format` must equal the format of the texture bound there. Likewise `depthStencil.enabled` requires the pass to bind a depth attachment of diff --git a/docs/graphics/rhi/spinning-cube.md b/docs/graphics/rhi/spinning-cube.md index a1b8538f2..a36b546bd 100644 --- a/docs/graphics/rhi/spinning-cube.md +++ b/docs/graphics/rhi/spinning-cube.md @@ -39,8 +39,7 @@ The cube uses a vertex + fragment shader pair. With the transpiler enabled, GLSL ```cpp GpuPipelineOptions options; -options.vertexBuffers = &cubeLayout; // position/color/normal -options.vertexBufferCount = 1; +options.vertexBuffers.push_back (cubeLayout); // position/color/normal options.indexFormat = GpuIndexFormat::uint16; options.cullMode = GpuCullMode::back; options.winding = GpuFaceWinding::counterClockwise; diff --git a/docs/graphics/rhi/targets.md b/docs/graphics/rhi/targets.md index 03efb26d3..51d8886ef 100644 --- a/docs/graphics/rhi/targets.md +++ b/docs/graphics/rhi/targets.md @@ -88,9 +88,14 @@ lower-level `GpuDevice::createOffscreenTarget` / `beginOffscreen` / `endOffscreen` API. ```cpp -static GpuCanvas::Ptr GpuCanvas::create (GpuDevice::Ptr ctx, int width, int height); +static GpuCanvas::Ptr GpuCanvas::create (GraphicsContext& ctx, int width, int height, + std::optional clearColor = Colors::transparentBlack); ``` +Unlike `GpuTarget::create()`, which takes a `GpuDevice::Ptr`, the canvas takes the +`GraphicsContext`: its 2D drawing path constructs a `Graphics`, and every +`Graphics` constructor requires a graphics context. + ### 2D drawing path ```cpp @@ -106,10 +111,15 @@ if (canvas != nullptr) } ``` -`beginDraw()` opens (or reopens) a 2D frame and returns the `Graphics` to draw -into. On the first call it opens a fresh offscreen 2D GPU frame; subsequent calls -discard the previous frame's `Graphics` and reopen a new one on the same -already-allocated target, avoiding per-frame GPU resource reallocation. +`beginDraw (const GpuFrameDescriptor& frameDesc = {})` opens (or reopens) a 2D +frame and returns the `Graphics` to draw into. On the first call it opens a +fresh offscreen 2D GPU frame; subsequent calls discard the previous frame's +`Graphics` and reopen a new one on the same already-allocated target, avoiding +per-frame GPU resource reallocation. `frameDesc` gives control over +`msaaSampleCount`, `ditherMode`, `loadOp` and `clearColor`; its +`renderTargetWidth`/`renderTargetHeight` are ignored and auto-filled from the +canvas. The default `{}` reproduces the previous behaviour (clear to +transparent black, no msaa). ### Custom-pass path @@ -127,7 +137,7 @@ pass.finish(); | `getTarget()` | The underlying `GpuTarget` backing this canvas. | | `getWidth()` / `getHeight()` | Canvas dimensions in pixels. | | `beginRenderPass (frame, options)` | Begins a render pass targeting the backing texture. | -| `beginDraw()` | Opens/reopens a 2D frame; returns the `Graphics` to draw into. | +| `beginDraw (frameDesc = {})` | Opens/reopens a 2D frame; returns the `Graphics` to draw into. | | `commit()` | Finalizes an open 2D command. Usually unnecessary (auto-commits). | | `asTexture()` | GPU-texture view; auto-commits an open 2D frame. | | `asImage()` | `Image` with GPU texture + CPU pixels; auto-commits. | diff --git a/docs/scripting/index.md b/docs/scripting/index.md index 808351492..d7c4c914e 100644 --- a/docs/scripting/index.md +++ b/docs/scripting/index.md @@ -13,3 +13,12 @@ API-surface documentation for the Python bindings are still to come. - **Bindings** - the pybind11-based bridge to YUP core and graphics types. - **Embedding** - driving YUP from a Python host. +- [**GPU rendering from Python**](python-rhi.md) - what differs between the + `yup_rhi` C++ API and its Python bindings. + +```{toctree} +:hidden: +:maxdepth: 2 + +python-rhi +``` diff --git a/docs/scripting/python-rhi.md b/docs/scripting/python-rhi.md new file mode 100644 index 000000000..ff851e08c --- /dev/null +++ b/docs/scripting/python-rhi.md @@ -0,0 +1,204 @@ +# GPU rendering from Python + +The `yup_rhi` layer is exposed to Python more or less one-to-one with the C++ +API described under [RHI](../graphics/rhi/index.md), so that page remains the +reference for what each call means. This page covers only what is different in +Python. + +Working end-to-end scripts live in `python/demos`: `gpu_triangle.py` (fullscreen +pass), `gpu_effects.py` (2D canvas feeding a post-process pass) and +`gpu_cube.py` (vertex + index buffers, an uploaded texture, a sampler and a +depth attachment). + +## Getting a device + +RHI factories take a `GpuDevice`, not the `GraphicsContext` a `Graphics` hands +you, so go through `getGpuDevice()`: + +```python +def paint(self, g: yup.Graphics): + ctx = g.getGraphicsContext() + if ctx is None or not ctx.isGpuAvailable(): + return + + device = ctx.getGpuDevice() +``` + +Off-screen and headless work can create a device directly. It returns `None` +rather than raising when the backend is unavailable: + +```python +device = yup.GpuDevice.create(yup.GpuPlatform.Headless, yup.GpuDevice.Options()) +if device is None: + ... +``` + +## Failures raise, they do not return + +The C++ factories that return `ResultValue` raise a `RuntimeError` carrying +the error message instead: + +```python +try: + pipeline = yup.GpuPipeline.compileFromGlsl(device, vertGlsl, fragGlsl, options) +except RuntimeError as error: + print(f"compile failed: {error}") +``` + +Factories that return a null pointer in C++ - `GpuTarget.create`, +`GpuTexture.create`, `GpuSampler.create`, `GpuBuffer.create` - return `None`. + +`GpuShaderSource` owns its blob fields, so it is fully exposed. `code`, +`bindingMap` and `glFixup` are bytes-in, bytes-out properties accepting any +object supporting the buffer protocol, and read back as `bytes`: + +```python +source = yup.GpuShaderSource() +source.language = yup.GpuShaderLanguage.glsl +source.code = vertexGlslBytes +source.bindingMap = bindingMapBlob +pipeline = yup.GpuPipeline.compile(device, source, fragmentSource, options) +``` + +## Descriptors own their data, and convert by value + +`GpuVertexBufferLayout.attributes`, `GpuPipelineOptions.vertexBuffers` and +`GpuPipelineOptions.colorTargets` are `std::vector` members. pybind11 converts +them **by value**, so assign a whole list; mutating the list you read back does +nothing: + +```python +options = yup.GpuPipelineOptions() + +options.vertexBuffers = [ + yup.GpuVertexBufferLayout( + 44, # stride in bytes + yup.GpuVertexStepMode.vertex, + [ + yup.GpuVertexAttribute(yup.GpuVertexFormat.float3, 0, 0), + yup.GpuVertexAttribute(yup.GpuVertexFormat.float2, 36, 3), + ], + ), +] + +target = yup.GpuColorTarget() +target.format = yup.GpuTextureFormat.rgba8unorm +options.colorTargets = [target] # NOT options.colorTargets.append(...) +``` + +## Passing bytes + +Everything that takes raw memory - `GpuRenderPass.setUniformBuffer`, +`GpuComputePass.setUniformBuffer`, `GpuBuffer.create`, `GpuDevice.createBuffer` +/ `updateBuffer` / `readBuffer`, `GpuTexture.upload` - accepts any object +supporting the buffer protocol (`bytes`, `bytearray`, `memoryview`, a numpy +array) and is sized in bytes: + +```python +uniforms = struct.pack("<4f", angleY, angleX, aspect, 0.0) +rp.setUniformBuffer(0, 0, uniforms) +``` + +`readBuffer` writes into a *writable* buffer, so pass a `bytearray` or a numpy +array rather than `bytes`. `GpuTarget.readPixels()` is the exception: it takes no +argument and returns `width * height * 4` RGBA bytes, or `None` when readback is +unavailable. Only a target created by the `width`/`height` overload can be read +back - one backed by a directly allocated texture always returns `None`. + +`GpuTexture.upload` takes the pixels separately from the region that places +them, since `GpuTextureDataDesc` has no exposed data pointer: + +```python +region = yup.GpuTextureDataDesc() +region.width = region.height = 64 +texture.upload(pixels, region) +``` + +## Frames and passes are context managers + +`GpuFrame`, `GpuRenderPass` and `GpuComputePass` are move-only RAII types in +C++, and support `with` in Python. Leaving the block submits the frame or +finishes the pass: + +```python +with yup.GpuFrame.begin(device) as frame: + with target.beginRenderPass(frame, yup.GpuRenderOptions(True, yup.GpuColor.black())) as rp: + rp.setPipeline(pipeline) + rp.draw(3) +``` + +A pass borrows the frame it records into, so the bindings keep the frame (and +the target) alive for as long as the pass object exists. + +## Compute + +Compute mirrors the render path: compile a `GpuComputePipeline`, open a +`GpuComputePass`, bind storage buffers and uniforms, dispatch. `GpuDevice.isComputeAvailable()` +gates the whole thing. + +```python +if not device.isComputeAvailable(): + return + +pipeline = yup.GpuComputePipeline.compileFromGlsl( + device, COMPUTE_GLSL, yup.GpuWorkgroupSize(64, 1, 1)) + +data = struct.pack(f"<{count}f", *values) +buffer = yup.GpuBuffer.create(device, yup.GpuBufferType.storage, data) + +with yup.GpuComputePass.begin(device) as pass_: + pass_.setPipeline(pipeline) + pass_.setStorageBuffer(0, 0, buffer) + pass_.setUniformBuffer(0, 1, struct.pack(" { + { yup::GpuVertexFormat::float2, 0, 0 }, // center + { yup::GpuVertexFormat::float2, 8, 1 }, // offset + { yup::GpuVertexFormat::float4, 16, 2 }, // color + { yup::GpuVertexFormat::float2, 32, 3 }, // size (x,y) + }); - yup::GpuPipelineOptions pipelineOpts; - pipelineOpts.vertexBuffers = &kVertexLayout; - pipelineOpts.vertexBufferCount = 1; pipelineOpts.topology = yup::GpuPrimitiveTopology::triangleList; pipelineOpts.cullMode = yup::GpuCullMode::none; - pipelineOpts.colorTargets[0].blendEnabled = false; - pipelineOpts.colorTargetCount = 1; + pipelineOpts.colorTargets.emplace_back().blendEnabled = false; auto renderResult = yup::GpuPipeline::compileFromGlsl (device, vertSource, fragSource, pipelineOpts); if (renderResult.failed()) diff --git a/examples/graphics/source/examples/FluidSimulationDemo.h b/examples/graphics/source/examples/FluidSimulationDemo.h index 0b0e6363e..89c603a4e 100644 --- a/examples/graphics/source/examples/FluidSimulationDemo.h +++ b/examples/graphics/source/examples/FluidSimulationDemo.h @@ -968,11 +968,9 @@ void main() { fragmentSource += yup::String::fromUTF8 (part); yup::GpuPipelineOptions options; - options.vertexBufferCount = 0; options.topology = yup::GpuPrimitiveTopology::triangleList; options.cullMode = yup::GpuCullMode::none; - options.colorTargetCount = 1; - options.colorTargets[0].blendEnabled = false; // passes overwrite every pixel + options.colorTargets.emplace_back().blendEnabled = false; // passes overwrite every pixel auto result = yup::GpuPipeline::compileFromGlsl ( device, diff --git a/examples/graphics/source/examples/PbrDemo.h b/examples/graphics/source/examples/PbrDemo.h index 9724e4347..3d2f29195 100644 --- a/examples/graphics/source/examples/PbrDemo.h +++ b/examples/graphics/source/examples/PbrDemo.h @@ -627,44 +627,39 @@ class PbrDemo : public yup::Component static yup::GpuPipelineOptions bakePipelineOptions (yup::GpuTextureFormat format) { yup::GpuPipelineOptions options; - options.colorTargetCount = 1; - options.colorTargets[0].format = format; - options.colorTargets[0].blendEnabled = false; + auto& colorTarget = options.colorTargets.emplace_back(); + colorTarget.format = format; + colorTarget.blendEnabled = false; return options; } - static const yup::GpuVertexBufferLayout* sceneVertexLayouts() + static std::vector sceneVertexLayouts() { - static constexpr yup::GpuVertexAttribute meshAttributes[3] = { - { yup::GpuVertexFormat::float3, 0, 0 }, - { yup::GpuVertexFormat::float3, 12, 1 }, - { yup::GpuVertexFormat::float2, 24, 2 }, + return { + { (uint32_t) sizeof (PbrVertex), yup::GpuVertexStepMode::vertex, { + { yup::GpuVertexFormat::float3, 0, 0 }, + { yup::GpuVertexFormat::float3, 12, 1 }, + { yup::GpuVertexFormat::float2, 24, 2 }, + } }, + { (uint32_t) sizeof (InstanceMaterial), yup::GpuVertexStepMode::instance, { + { yup::GpuVertexFormat::float4, 0, 3 }, // a_material + { yup::GpuVertexFormat::float4, 16, 4 }, // a_tint + { yup::GpuVertexFormat::float4, 32, 5 }, // a_extra + } }, }; - - static constexpr yup::GpuVertexAttribute instanceAttributes[3] = { - { yup::GpuVertexFormat::float4, 0, 3 }, // a_material - { yup::GpuVertexFormat::float4, 16, 4 }, // a_tint - { yup::GpuVertexFormat::float4, 32, 5 }, // a_extra - }; - - static constexpr yup::GpuVertexBufferLayout layouts[2] = { - { (uint32_t) sizeof (PbrVertex), yup::GpuVertexStepMode::vertex, meshAttributes, 3 }, - { (uint32_t) sizeof (InstanceMaterial), yup::GpuVertexStepMode::instance, instanceAttributes, 3 }, - }; - - return layouts; } static yup::GpuPipelineOptions scenePipelineOptions() { yup::GpuPipelineOptions options; options.vertexBuffers = sceneVertexLayouts(); - options.vertexBufferCount = 2; options.indexFormat = yup::GpuIndexFormat::uint16; options.cullMode = yup::GpuCullMode::none; - options.colorTargetCount = 1; - options.colorTargets[0].format = yup::GpuTextureFormat::rgba8unorm; - options.colorTargets[0].blendEnabled = false; + + auto& colorTarget = options.colorTargets.emplace_back(); + colorTarget.format = yup::GpuTextureFormat::rgba8unorm; + colorTarget.blendEnabled = false; + options.depthStencil.enabled = true; options.depthStencil.format = yup::GpuTextureFormat::depth24plusStencil8; options.depthStencil.depthCompare = yup::GpuCompareFunction::less; diff --git a/examples/graphics/source/examples/SpinningCubeDemo.h b/examples/graphics/source/examples/SpinningCubeDemo.h index 445d58f47..78a6a2acd 100644 --- a/examples/graphics/source/examples/SpinningCubeDemo.h +++ b/examples/graphics/source/examples/SpinningCubeDemo.h @@ -555,23 +555,16 @@ void main() { /** Builds the pipeline options describing the cube's vertex layout and state. */ static yup::GpuPipelineOptions cubePipelineOptions() { - static constexpr yup::GpuVertexAttribute attrs[4] = { - { yup::GpuVertexFormat::float3, 0, 0 }, - { yup::GpuVertexFormat::float3, 12, 1 }, - { yup::GpuVertexFormat::float3, 24, 2 }, - { yup::GpuVertexFormat::float2, 36, 3 }, - }; - - static constexpr yup::GpuVertexBufferLayout vbLayout { + yup::GpuPipelineOptions options; + options.vertexBuffers.emplace_back ( (uint32_t) sizeof (CubeVertex), yup::GpuVertexStepMode::vertex, - attrs, - 4 - }; - - yup::GpuPipelineOptions options; - options.vertexBuffers = &vbLayout; - options.vertexBufferCount = 1; + std::vector { + { yup::GpuVertexFormat::float3, 0, 0 }, + { yup::GpuVertexFormat::float3, 12, 1 }, + { yup::GpuVertexFormat::float3, 24, 2 }, + { yup::GpuVertexFormat::float2, 36, 3 }, + }); options.topology = yup::GpuPrimitiveTopology::triangleList; options.indexFormat = yup::GpuIndexFormat::uint16; options.cullMode = yup::GpuCullMode::back; diff --git a/justfile b/justfile index d186c1654..e5d30a085 100644 --- a/justfile +++ b/justfile @@ -90,30 +90,33 @@ emscripten_test: [doc("serve project for WASM")] emscripten_serve: - #python3 -m http.server -d . - python3 tools/serve.py -p 8000 -d . + #uv run python -m http.server -d . + uv run python tools/serve.py -p 8000 -d . [doc("generate python wheel for yup_python bindings")] [working-directory: 'python'] python_wheel: - python -m build --wheel + uv venv --allow-existing + uv pip install build + uv run python -m build --wheel @just python_install @just python_test [doc("install python wheel for yup_python bindings")] [working-directory: 'python'] python_install: - python -m pip install --force-reinstall dist/yup-*.whl + uv pip install --force-reinstall dist/yup-*.whl [doc("uninstall python wheel for yup_python bindings")] [working-directory: 'python'] python_uninstall: - python -m pip uninstall -y yup + uv pip uninstall -y yup [doc("run tests for yup_python bindings")] [working-directory: 'python'] python_test *TEST_OPTS: - python -m pytest -s {{TEST_OPTS}} + uv sync --group test + uv run --group test python -m pytest -s {{TEST_OPTS}} [doc("compile and invoke shader_bundler tool")] [working-directory: 'cmake/tools/shader_bundler'] diff --git a/modules/yup_animation/renderer/yup_AnimationFrameExporter.cpp b/modules/yup_animation/renderer/yup_AnimationFrameExporter.cpp index 1124cae3e..0ea965df6 100644 --- a/modules/yup_animation/renderer/yup_AnimationFrameExporter.cpp +++ b/modules/yup_animation/renderer/yup_AnimationFrameExporter.cpp @@ -32,6 +32,7 @@ AnimationFrameExporter::AnimationFrameExporter (GraphicsContext& ctx) AnimationFrameExporter::~AnimationFrameExporter() = default; +//============================================================================== Size AnimationFrameExporter::resolveTargetSize (const Animation& anim, Size requested) { if (requested.getWidth() > 0 && requested.getHeight() > 0) @@ -41,6 +42,7 @@ Size AnimationFrameExporter::resolveTargetSize (const Animation& anim, Size return { (int) native.getWidth(), (int) native.getHeight() }; } +//============================================================================== Image AnimationFrameExporter::renderFrame (const Animation& anim, float frameNo, Size targetSize) @@ -63,6 +65,7 @@ Image AnimationFrameExporter::renderFrame (const Animation& anim, return img; } +//============================================================================== ResultValue> AnimationFrameExporter::renderAllFrames (const Animation& anim, Size targetSize) { if (! anim.isValid()) @@ -85,6 +88,8 @@ ResultValue> AnimationFrameExporter::renderAllFrames (const A return makeResultValueOk (std::move (frames)); } +//============================================================================== +#if YUP_IMAGE_FORMAT_GIF Result AnimationFrameExporter::exportToGif (const Animation& anim, const File& destination, Size targetSize, @@ -165,5 +170,6 @@ Result AnimationFrameExporter::exportToGif (const std::vector& frames, return Result::ok(); } +#endif } // namespace yup diff --git a/modules/yup_animation/renderer/yup_AnimationFrameExporter.h b/modules/yup_animation/renderer/yup_AnimationFrameExporter.h index 987cee00d..00386379e 100644 --- a/modules/yup_animation/renderer/yup_AnimationFrameExporter.h +++ b/modules/yup_animation/renderer/yup_AnimationFrameExporter.h @@ -88,6 +88,7 @@ class YUP_API AnimationFrameExporter [[nodiscard]] ResultValue> renderAllFrames (const Animation& anim, Size targetSize = {}); +#if YUP_IMAGE_FORMAT_GIF //============================================================================== /** Exports the animation to an animated GIF file. @@ -119,6 +120,7 @@ class YUP_API AnimationFrameExporter float frameRate, const File& destination, int qualityLevel = 80); +#endif private: static Size resolveTargetSize (const Animation& anim, Size requested); diff --git a/modules/yup_animation/renderer/yup_AnimationRenderResources.cpp b/modules/yup_animation/renderer/yup_AnimationRenderResources.cpp index 3802a1870..e87265ffa 100644 --- a/modules/yup_animation/renderer/yup_AnimationRenderResources.cpp +++ b/modules/yup_animation/renderer/yup_AnimationRenderResources.cpp @@ -145,9 +145,9 @@ GpuPipeline::Ptr AnimationRenderResources::getMattePipeline (GraphicsContext& co // src-alpha / one-minus-src-alpha blend would premultiply the RGB a second // time and darken the result (white matte -> grey). GpuPipelineOptions options; - options.colorTargetCount = 1; - options.colorTargets[0].format = GpuTextureFormat::rgba8unorm; - options.colorTargets[0].blendEnabled = false; + auto& colorTarget = options.colorTargets.emplace_back(); + colorTarget.format = GpuTextureFormat::rgba8unorm; + colorTarget.blendEnabled = false; auto result = GpuPipeline::compileFromGlsl (context.getGpuDevice(), String::fromUTF8 (kMatteVertSource, (int) sizeof (kMatteVertSource) - 1), diff --git a/modules/yup_audio_devices/yup_audio_devices.h b/modules/yup_audio_devices/yup_audio_devices.h index a93d6cb91..106508db2 100644 --- a/modules/yup_audio_devices/yup_audio_devices.h +++ b/modules/yup_audio_devices/yup_audio_devices.h @@ -1,212 +1,212 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -/* - ============================================================================== - - BEGIN_YUP_MODULE_DECLARATION - - ID: yup_audio_devices - vendor: yup - version: 2.0.0 - name: YUP audio and MIDI I/O device classes - description: Classes to play and record from audio and MIDI I/O devices - website: https://github.com/kunitoki/yup - license: ISC - - dependencies: yup_audio_basics yup_events - optionalDeps: yup_graphics - appleFrameworks: CoreAudio CoreMIDI AudioToolbox - iosFrameworks: AVFoundation - iosSimFrameworks: AVFoundation - linuxPackages: alsa - androidDeps: oboe_library - - END_YUP_MODULE_DECLARATION - - ============================================================================== -*/ - -#pragma once -#define YUP_AUDIO_DEVICES_H_INCLUDED - -#include -#include - -#if 0 && YUP_MODULE_AVAILABLE_yup_graphics -#include -#endif - -//============================================================================== -/** Config: YUP_USE_WINRT_MIDI - Enables the use of the Windows Runtime API for MIDI, allowing connections - to Bluetooth Low Energy devices on Windows 10 version 1809 (October 2018 - Update) and later. If you enable this flag then older versions of Windows - will automatically fall back to using the regular Win32 MIDI API. - - You will need version 10.0.14393.0 of the Windows Standalone SDK to compile - and you may need to add the path to the WinRT headers. The path to the - headers will be something similar to - "C:\Program Files (x86)\Windows Kits\10\Include\10.0.14393.0\winrt". -*/ -#ifndef YUP_USE_WINRT_MIDI -#define YUP_USE_WINRT_MIDI 0 -#endif - -/** Config: YUP_ASIO - Enables ASIO audio devices (MS Windows only). - Turning this on means that you'll need to have the Steinberg ASIO SDK installed - on your Windows build machine. - - See the comments in the ASIOAudioIODevice class's header file for more - info about this. -*/ -#ifndef YUP_ASIO -#define YUP_ASIO 0 -#endif - -/** Config: YUP_WASAPI - Enables WASAPI audio devices (Windows Vista and above). -*/ -#ifndef YUP_WASAPI -#define YUP_WASAPI 1 -#endif - -/** Config: YUP_DIRECTSOUND - Enables DirectSound audio (MS Windows only). -*/ -#ifndef YUP_DIRECTSOUND -#define YUP_DIRECTSOUND 1 -#endif - -/** Config: YUP_ALSA - Enables ALSA audio devices (Linux only). -*/ -#ifndef YUP_ALSA -#define YUP_ALSA 1 -#endif - -/** Config: YUP_JACK - Enables JACK audio devices. -*/ -#ifndef YUP_JACK -#define YUP_JACK 0 -#endif - -/** Config: YUP_BELA - Enables Bela audio devices on Bela boards. -*/ -#ifndef YUP_BELA -#define YUP_BELA 0 -#endif - -/** Config: YUP_USE_ANDROID_OBOE - Enables Oboe devices (Android only). -*/ -#ifndef YUP_USE_ANDROID_OBOE -#define YUP_USE_ANDROID_OBOE 1 -#endif - -/** Config: YUP_USE_OBOE_STABILIZED_CALLBACK - If YUP_USE_ANDROID_OBOE is enabled, enabling this will wrap output audio - streams in the oboe::StabilizedCallback class. This class attempts to keep - the CPU spinning to avoid it being scaled down on certain devices. - (Android only). -*/ -#ifndef YUP_USE_ANDROID_OBOE_STABILIZED_CALLBACK -#define YUP_USE_ANDROID_OBOE_STABILIZED_CALLBACK 0 -#endif - -/** Config: YUP_USE_ANDROID_OPENSLES - Enables OpenSLES devices (Android only). -*/ -#ifndef YUP_USE_ANDROID_OPENSLES -#if ! YUP_USE_ANDROID_OBOE -#define YUP_USE_ANDROID_OPENSLES 1 -#else -#define YUP_USE_ANDROID_OPENSLES 0 -#endif -#endif - -/** Config: YUP_DISABLE_AUDIO_MIXING_WITH_OTHER_APPS - Turning this on gives your app exclusive access to the system's audio - on platforms which support it (currently iOS only). -*/ -#ifndef YUP_DISABLE_AUDIO_MIXING_WITH_OTHER_APPS -#define YUP_DISABLE_AUDIO_MIXING_WITH_OTHER_APPS 0 -#endif - -/** Config: YUP_ENABLE_CORE_AUDIO_LOGGING - - Enable logging of audio device events on macOS, such as device changes and errors. -*/ -#ifndef YUP_ENABLE_CORE_AUDIO_LOGGING -#define YUP_ENABLE_CORE_AUDIO_LOGGING 0 -#endif - -//============================================================================== -#include "midi_io/yup_MidiDevices.h" -#include "midi_io/yup_MidiMessageCollector.h" -#include "midi_io/ump/yup_UMPPacketCollector.h" - -namespace yup -{ -/** Available modes for the WASAPI audio device. - - Pass one of these to the AudioIODeviceType::createAudioIODeviceType_WASAPI() - method to create a WASAPI AudioIODeviceType object in this mode. -*/ -enum class WASAPIDeviceMode -{ - shared, - exclusive, - sharedLowLatency -}; -} // namespace yup - -#include "audio_io/yup_AudioIODevice.h" -#include "audio_io/yup_AudioIODeviceType.h" -#include "audio_io/yup_SystemAudioVolume.h" -#include "sources/yup_AudioSourcePlayer.h" -#include "sources/yup_AudioTransportSource.h" -#include "audio_io/yup_AudioDeviceManager.h" - -#if YUP_IOS -#include "native/yup_Audio_ios.h" -#endif +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +/* + ============================================================================== + + BEGIN_YUP_MODULE_DECLARATION + + ID: yup_audio_devices + vendor: yup + version: 2.0.0 + name: YUP audio and MIDI I/O device classes + description: Classes to play and record from audio and MIDI I/O devices + website: https://github.com/kunitoki/yup + license: ISC + + dependencies: yup_audio_basics yup_events + optionalDeps: yup_graphics yup_audio_formats + appleFrameworks: CoreAudio CoreMIDI AudioToolbox + iosFrameworks: AVFoundation + iosSimFrameworks: AVFoundation + linuxPackages: alsa + androidDeps: oboe_library + + END_YUP_MODULE_DECLARATION + + ============================================================================== +*/ + +#pragma once +#define YUP_AUDIO_DEVICES_H_INCLUDED + +#include +#include + +#if 0 && YUP_MODULE_AVAILABLE_yup_graphics +#include +#endif + +//============================================================================== +/** Config: YUP_USE_WINRT_MIDI + Enables the use of the Windows Runtime API for MIDI, allowing connections + to Bluetooth Low Energy devices on Windows 10 version 1809 (October 2018 + Update) and later. If you enable this flag then older versions of Windows + will automatically fall back to using the regular Win32 MIDI API. + + You will need version 10.0.14393.0 of the Windows Standalone SDK to compile + and you may need to add the path to the WinRT headers. The path to the + headers will be something similar to + "C:\Program Files (x86)\Windows Kits\10\Include\10.0.14393.0\winrt". +*/ +#ifndef YUP_USE_WINRT_MIDI +#define YUP_USE_WINRT_MIDI 0 +#endif + +/** Config: YUP_ASIO + Enables ASIO audio devices (MS Windows only). + Turning this on means that you'll need to have the Steinberg ASIO SDK installed + on your Windows build machine. + + See the comments in the ASIOAudioIODevice class's header file for more + info about this. +*/ +#ifndef YUP_ASIO +#define YUP_ASIO 0 +#endif + +/** Config: YUP_WASAPI + Enables WASAPI audio devices (Windows Vista and above). +*/ +#ifndef YUP_WASAPI +#define YUP_WASAPI 1 +#endif + +/** Config: YUP_DIRECTSOUND + Enables DirectSound audio (MS Windows only). +*/ +#ifndef YUP_DIRECTSOUND +#define YUP_DIRECTSOUND 1 +#endif + +/** Config: YUP_ALSA + Enables ALSA audio devices (Linux only). +*/ +#ifndef YUP_ALSA +#define YUP_ALSA 1 +#endif + +/** Config: YUP_JACK + Enables JACK audio devices. +*/ +#ifndef YUP_JACK +#define YUP_JACK 0 +#endif + +/** Config: YUP_BELA + Enables Bela audio devices on Bela boards. +*/ +#ifndef YUP_BELA +#define YUP_BELA 0 +#endif + +/** Config: YUP_USE_ANDROID_OBOE + Enables Oboe devices (Android only). +*/ +#ifndef YUP_USE_ANDROID_OBOE +#define YUP_USE_ANDROID_OBOE 1 +#endif + +/** Config: YUP_USE_OBOE_STABILIZED_CALLBACK + If YUP_USE_ANDROID_OBOE is enabled, enabling this will wrap output audio + streams in the oboe::StabilizedCallback class. This class attempts to keep + the CPU spinning to avoid it being scaled down on certain devices. + (Android only). +*/ +#ifndef YUP_USE_ANDROID_OBOE_STABILIZED_CALLBACK +#define YUP_USE_ANDROID_OBOE_STABILIZED_CALLBACK 0 +#endif + +/** Config: YUP_USE_ANDROID_OPENSLES + Enables OpenSLES devices (Android only). +*/ +#ifndef YUP_USE_ANDROID_OPENSLES +#if ! YUP_USE_ANDROID_OBOE +#define YUP_USE_ANDROID_OPENSLES 1 +#else +#define YUP_USE_ANDROID_OPENSLES 0 +#endif +#endif + +/** Config: YUP_DISABLE_AUDIO_MIXING_WITH_OTHER_APPS + Turning this on gives your app exclusive access to the system's audio + on platforms which support it (currently iOS only). +*/ +#ifndef YUP_DISABLE_AUDIO_MIXING_WITH_OTHER_APPS +#define YUP_DISABLE_AUDIO_MIXING_WITH_OTHER_APPS 0 +#endif + +/** Config: YUP_ENABLE_CORE_AUDIO_LOGGING + + Enable logging of audio device events on macOS, such as device changes and errors. +*/ +#ifndef YUP_ENABLE_CORE_AUDIO_LOGGING +#define YUP_ENABLE_CORE_AUDIO_LOGGING 0 +#endif + +//============================================================================== +#include "midi_io/yup_MidiDevices.h" +#include "midi_io/yup_MidiMessageCollector.h" +#include "midi_io/ump/yup_UMPPacketCollector.h" + +namespace yup +{ +/** Available modes for the WASAPI audio device. + + Pass one of these to the AudioIODeviceType::createAudioIODeviceType_WASAPI() + method to create a WASAPI AudioIODeviceType object in this mode. +*/ +enum class WASAPIDeviceMode +{ + shared, + exclusive, + sharedLowLatency +}; +} // namespace yup + +#include "audio_io/yup_AudioIODevice.h" +#include "audio_io/yup_AudioIODeviceType.h" +#include "audio_io/yup_SystemAudioVolume.h" +#include "sources/yup_AudioSourcePlayer.h" +#include "sources/yup_AudioTransportSource.h" +#include "audio_io/yup_AudioDeviceManager.h" + +#if YUP_IOS +#include "native/yup_Audio_ios.h" +#endif diff --git a/modules/yup_audio_formats/sources/yup_AudioFormatReaderSource.cpp b/modules/yup_audio_formats/sources/yup_AudioFormatReaderSource.cpp new file mode 100644 index 000000000..3a6e90b34 --- /dev/null +++ b/modules/yup_audio_formats/sources/yup_AudioFormatReaderSource.cpp @@ -0,0 +1,170 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +AudioFormatReaderSource::AudioFormatReaderSource (AudioFormatReader* sourceReader, + bool deleteReaderWhenThisIsDeleted) + : reader (sourceReader) + , deleteReader (deleteReaderWhenThisIsDeleted) +{ + // Allow null reader — source produces silence +} + +AudioFormatReaderSource::AudioFormatReaderSource (std::unique_ptr sourceReader) + : ownedReader (std::move (sourceReader)) + , reader (ownedReader.get()) + , deleteReader (false) +{ +} + +AudioFormatReaderSource::~AudioFormatReaderSource() +{ + if (reader != nullptr && deleteReader) + delete reader; +} + +//============================================================================== +int64 AudioFormatReaderSource::getTotalLength() const +{ + if (reader == nullptr) + return 0; + return reader->lengthInSamples; +} + +void AudioFormatReaderSource::setNextReadPosition (int64 newPosition) +{ + if (newPosition < 0) + newPosition = 0; + + nextReadPosition = newPosition; +} + +int64 AudioFormatReaderSource::getNextReadPosition() const +{ + return nextReadPosition; +} + +bool AudioFormatReaderSource::isLooping() const +{ + return looping; +} + +void AudioFormatReaderSource::setLooping (bool shouldLoop) +{ + looping = shouldLoop; +} + +//============================================================================== +void AudioFormatReaderSource::prepareToPlay (int /*samplesPerBlockExpected*/, + double /*sampleRate*/) +{ +} + +void AudioFormatReaderSource::releaseResources() +{ +} + +void AudioFormatReaderSource::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) +{ + if (reader == nullptr) + { + bufferToFill.clearActiveBufferRegion(); + return; + } + + const auto totalLength = getTotalLength(); + + if (totalLength > 0) + { + auto samplesAvailable = totalLength - nextReadPosition; + + if (samplesAvailable < bufferToFill.numSamples) + { + if (looping) + { + auto samplesNeeded = bufferToFill.numSamples; + auto firstChunk = static_cast (samplesAvailable); + auto secondChunk = samplesNeeded - firstChunk; + + // Read first chunk from the end of the file + if (firstChunk > 0) + { + reader->read (bufferToFill.buffer, + bufferToFill.startSample, + firstChunk, + nextReadPosition, + true, + true); + } + + // Read second chunk from the beginning of the file + if (secondChunk > 0) + { + reader->read (bufferToFill.buffer, + bufferToFill.startSample + firstChunk, + secondChunk, + 0, + true, + true); + } + + nextReadPosition = secondChunk; + } + else + { + // Read what's left and clear the rest + auto numToRead = static_cast (samplesAvailable); + + reader->read (bufferToFill.buffer, + bufferToFill.startSample, + numToRead, + nextReadPosition, + true, + true); + + bufferToFill.buffer->clear (bufferToFill.startSample + numToRead, + bufferToFill.numSamples - numToRead); + + nextReadPosition = totalLength; + } + } + else + { + reader->read (bufferToFill.buffer, + bufferToFill.startSample, + bufferToFill.numSamples, + nextReadPosition, + true, + true); + + nextReadPosition += bufferToFill.numSamples; + } + } + else + { + bufferToFill.clearActiveBufferRegion(); + } +} + +} // namespace yup diff --git a/modules/yup_audio_formats/sources/yup_AudioFormatReaderSource.h b/modules/yup_audio_formats/sources/yup_AudioFormatReaderSource.h new file mode 100644 index 000000000..3826a5dff --- /dev/null +++ b/modules/yup_audio_formats/sources/yup_AudioFormatReaderSource.h @@ -0,0 +1,98 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + A PositionableAudioSource that reads from an AudioFormatReader. + + This class wraps an AudioFormatReader, turning it into a PositionableAudioSource + that can be used with AudioTransportSource for playback of audio files. + + This is the simplest way to read from an audio file: create an AudioFormatReader + for the file, wrap it in an AudioFormatReaderSource, pass it to an + AudioTransportSource, and play. + + @see AudioFormatReader, AudioTransportSource, PositionableAudioSource + + @tags{Audio} +*/ +class YUP_API AudioFormatReaderSource : public PositionableAudioSource +{ +public: + //============================================================================== + /** Creates an AudioFormatReaderSource from an AudioFormatReader. + + @param sourceReader the reader to use as the source. The + AudioFormatReaderSource will take ownership + of this reader and delete it when no longer needed. + @param deleteReaderWhenThisIsDeleted if true, the sourceReader will be deleted + when this object is destroyed + */ + AudioFormatReaderSource (AudioFormatReader* sourceReader, + bool deleteReaderWhenThisIsDeleted); + + /** Creates an AudioFormatReaderSource from a unique_ptr. + Takes ownership of the reader. + */ + explicit AudioFormatReaderSource (std::unique_ptr sourceReader); + + /** Destructor. */ + ~AudioFormatReaderSource() override; + + //============================================================================== + /** Returns the AudioFormatReader being used as the source. */ + AudioFormatReader* getAudioFormatReader() const noexcept { return reader; } + + //============================================================================== + /** @internal */ + void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override; + /** @internal */ + void releaseResources() override; + /** @internal */ + void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override; + + //============================================================================== + /** @internal */ + void setNextReadPosition (int64 newPosition) override; + /** @internal */ + int64 getNextReadPosition() const override; + /** @internal */ + int64 getTotalLength() const override; + /** @internal */ + bool isLooping() const override; + /** @internal */ + void setLooping (bool shouldLoop) override; + +private: + //============================================================================== + std::unique_ptr ownedReader; + AudioFormatReader* reader = nullptr; + bool deleteReader = false; + int64 nextReadPosition = 0; + bool looping = false; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioFormatReaderSource) +}; + +} // namespace yup diff --git a/modules/yup_audio_formats/yup_audio_formats.cpp b/modules/yup_audio_formats/yup_audio_formats.cpp index 4ee94a055..87bf2120a 100644 --- a/modules/yup_audio_formats/yup_audio_formats.cpp +++ b/modules/yup_audio_formats/yup_audio_formats.cpp @@ -82,6 +82,7 @@ #include "format/yup_AudioFormatReader.cpp" #include "format/yup_AudioFormatWriter.cpp" #include "common/yup_AudioFormatManager.cpp" +#include "sources/yup_AudioFormatReaderSource.cpp" //============================================================================== diff --git a/modules/yup_audio_formats/yup_audio_formats.h b/modules/yup_audio_formats/yup_audio_formats.h index 6334c0c61..a8e89982c 100644 --- a/modules/yup_audio_formats/yup_audio_formats.h +++ b/modules/yup_audio_formats/yup_audio_formats.h @@ -168,6 +168,7 @@ #include "format/yup_AudioFormatReader.h" #include "format/yup_AudioFormatWriter.h" #include "common/yup_AudioFormatManager.h" +#include "sources/yup_AudioFormatReaderSource.h" //============================================================================== diff --git a/modules/yup_audio_gui/displays/yup_CartesianPlane.h b/modules/yup_audio_gui/displays/yup_CartesianPlane.h index 90ebad648..893e07891 100644 --- a/modules/yup_audio_gui/displays/yup_CartesianPlane.h +++ b/modules/yup_audio_gui/displays/yup_CartesianPlane.h @@ -150,7 +150,7 @@ class YUP_API CartesianPlane : public Component // Title configuration /** Set the plot title */ - void setTitle (const String& title); + void setTitle (const String& title) override; /** Get the current title */ const String& getTitle() const { return titleText; } diff --git a/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.mm b/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.mm index ca0e51f46..754f719ba 100644 --- a/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.mm +++ b/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.mm @@ -602,6 +602,8 @@ void buildInputBusViews (const AudioBufferList& mainInBuffer) const int numCh = bus.getNumChannels(); auto* chPtrs = renderViews.inputChannelPtrStorage.data() + chOffset; + std::fill (chPtrs, chPtrs + numCh, nullptr); + if (audioIdx == 0) { const UInt32 copyCount = std::min (mainInBuffer.mNumberBuffers, static_cast (numCh)); diff --git a/modules/yup_core/containers/yup_TypeErasedObject.h b/modules/yup_core/containers/yup_TypeErasedObject.h index bc8fe0266..de5e748c8 100644 --- a/modules/yup_core/containers/yup_TypeErasedObject.h +++ b/modules/yup_core/containers/yup_TypeErasedObject.h @@ -29,6 +29,9 @@ namespace yup The `TypeErasedObject` template struct stores an object of a specified type in a type-erased manner, provided that the size of the object is less than or equal to the specified `NumBytes`. This struct ensures that objects are move-only, and it uses type erasure to store them in a generic way while still allowing retrieval of the original type at a later point. + + Moving a `TypeErasedObject` relocates the payload through the payload's own move constructor, so + types that point into themselves (such as `std::string` or `std::map`) are safe to store. @tparam NumBytes The maximum number of bytes available for storing the payload object. @@ -61,95 +64,88 @@ struct TypeErasedObject { destroyAt (std::launder (reinterpret_cast (buffer))); }; + + moverCallback = +[] (void* destination, void* source) + { + auto* sourceObject = std::launder (reinterpret_cast (source)); + constructAt (reinterpret_cast (destination), std::move (*sourceObject)); + destroyAt (sourceObject); + }; } /** Destroys the payload and calls the stored deleter to clean up the contained object. */ ~TypeErasedObject() { - if (deleterCallback != nullptr) - deleterCallback (static_cast (&objectBuffer[0])); + destroyPayload(); } /** Move constructor that transfers ownership of the payload from another instance. - - Moves the contents of the payload from `other` into this instance, ensuring that the - other instance is left in a valid but empty state. - + + Relocates the payload from `other` into this instance through the payload's own move + constructor, then destroys the source payload, leaving `other` in a valid but empty state. + @param other The payload to move from. */ TypeErasedObject (TypeErasedObject&& other) noexcept - : deleterCallback (std::exchange (other.deleterCallback, nullptr)) - , type (std::exchange (other.type, typeid (void))) { - std::memcpy (objectBuffer, other.objectBuffer, jmin (sizeof (objectBuffer), sizeof (other.objectBuffer))); + takePayloadFrom (other); } /** Move constructor that transfers ownership of the payload from a smaller instance. - - Moves the contents of the payload from `other`, whose storage size must be less than or equal - to this instance's storage size, ensuring that `other` is left in a valid but empty state. - + + Relocates the payload from `other`, whose storage size must be less than or equal to this + instance's storage size, through the payload's own move constructor, then destroys the + source payload, leaving `other` in a valid but empty state. + @tparam OtherBytes The storage size of the source payload. Must be less than or equal to `NumBytes`. - + @param other The payload to move from. */ template TypeErasedObject (TypeErasedObject&& other) noexcept requires (OtherBytes <= NumBytes) - : deleterCallback (std::exchange (other.deleterCallback, nullptr)) - , type (std::exchange (other.type, typeid (void))) { - std::memcpy (objectBuffer, other.objectBuffer, jmin (sizeof (objectBuffer), sizeof (other.objectBuffer))); + takePayloadFrom (other); } /** Move assignment operator that transfers ownership of the payload from another instance. - - Moves the contents of the payload from `other` into this instance, properly destroying the - current payload object if one exists, and leaving `other` in a valid but empty state. - + + Destroys the current payload object if one exists, then relocates the payload from `other` + through its own move constructor, leaving `other` in a valid but empty state. + @param other The payload to move from. - + @return A reference to this `TypeErasedObject` after the move. */ TypeErasedObject& operator= (TypeErasedObject&& other) { - if (auto deleter = std::exchange (deleterCallback, nullptr)) - deleter (reinterpret_cast (&objectBuffer[0])); - - deleterCallback = std::exchange (other.deleterCallback, nullptr); - type = std::exchange (other.type, typeid (void)); - std::memcpy (objectBuffer, other.objectBuffer, jmin (sizeof (objectBuffer), sizeof (other.objectBuffer))); - + destroyPayload(); + takePayloadFrom (other); return *this; } /** Move assignment operator that transfers ownership of the payload from a smaller instance. - - Moves the contents of the payload from `other`, whose storage size must be less than or equal - to this instance's storage size, properly destroying the current payload object if one exists, - and leaving `other` in a valid but empty state. - + + Destroys the current payload object if one exists, then relocates the payload from `other`, + whose storage size must be less than or equal to this instance's storage size, through its + own move constructor, leaving `other` in a valid but empty state. + @tparam OtherBytes The storage size of the source payload. Must be less than or equal to `NumBytes`. - + @param other The payload to move from. - + @return A reference to this `TypeErasedObject` after the move. */ template TypeErasedObject& operator= (TypeErasedObject&& other) requires (OtherBytes <= NumBytes) { - if (auto deleter = std::exchange (deleterCallback, nullptr)) - deleter (reinterpret_cast (&objectBuffer[0])); - - deleterCallback = std::exchange (other.deleterCallback, nullptr); - type = std::exchange (other.type, typeid (void)); - std::memcpy (objectBuffer, other.objectBuffer, jmin (sizeof (objectBuffer), sizeof (other.objectBuffer))); - + destroyPayload(); + takePayloadFrom (other); return *this; } @@ -201,8 +197,31 @@ struct TypeErasedObject template friend struct TypeErasedObject; + /** Destroys the current payload, if any, and returns to the empty state. */ + void destroyPayload() noexcept + { + if (auto deleter = std::exchange (deleterCallback, nullptr)) + deleter (static_cast (&objectBuffer[0])); + + moverCallback = nullptr; + type = typeid (void); + } + + /** Relocates the payload of `other` into this empty instance and empties `other`. */ + template + void takePayloadFrom (TypeErasedObject& other) noexcept + { + deleterCallback = std::exchange (other.deleterCallback, nullptr); + moverCallback = std::exchange (other.moverCallback, nullptr); + type = std::exchange (other.type, typeid (void)); + + if (moverCallback != nullptr) + moverCallback (static_cast (&objectBuffer[0]), static_cast (&other.objectBuffer[0])); + } + alignas (alignof (std::max_align_t)) uint8 objectBuffer[NumBytes] = {}; void (*deleterCallback) (void*) = nullptr; + void (*moverCallback) (void*, void*) = nullptr; std::type_index type = typeid (void); }; diff --git a/modules/yup_core/network/yup_IPAddress.cpp b/modules/yup_core/network/yup_IPAddress.cpp index 899ad3cab..0b28faed8 100644 --- a/modules/yup_core/network/yup_IPAddress.cpp +++ b/modules/yup_core/network/yup_IPAddress.cpp @@ -40,25 +40,44 @@ namespace yup { -/** Union used to split a 16-bit unsigned integer into 2 8-bit unsigned integers or vice-versa */ +namespace +{ + union IPAddressByteUnion { uint16 combined; uint8 split[2]; }; -static void zeroUnusedBytes (uint8* address) noexcept +void zeroUnusedBytes (uint8* address) noexcept { for (int i = 4; i < 16; ++i) address[i] = 0; } +uint16 getAddressGroup (const uint8* address, int groupIndex) noexcept +{ + const auto lowByte = static_cast (address[groupIndex * 2]); + const auto highByte = static_cast (address[groupIndex * 2 + 1]); + + return static_cast ((highByte << 8) | lowByte); +} + +void setAddressGroup (uint8* address, int groupIndex, uint16 value) noexcept +{ + address[groupIndex * 2] = static_cast (value & 0xff); + address[groupIndex * 2 + 1] = static_cast (value >> 8); +} + +} // namespace + IPAddress::IPAddress() noexcept { for (int i = 0; i < 16; ++i) address[i] = 0; } + IPAddress::IPAddress (const uint8 bytes[], bool IPv6) noexcept : isIPv6 (IPv6) { @@ -72,15 +91,8 @@ IPAddress::IPAddress (const uint8 bytes[], bool IPv6) noexcept IPAddress::IPAddress (const uint16 bytes[8]) noexcept : isIPv6 (true) { - IPAddressByteUnion temp; - for (int i = 0; i < 8; ++i) - { - temp.combined = bytes[i]; - - address[i * 2] = temp.split[0]; - address[i * 2 + 1] = temp.split[1]; - } + setAddressGroup (address, i, bytes[i]); } IPAddress::IPAddress (uint8 a0, uint8 a1, uint8 a2, uint8 a3) noexcept @@ -100,14 +112,8 @@ IPAddress::IPAddress (uint16 a1, uint16 a2, uint16 a3, uint16 a4, uint16 a5, uin { uint16 array[8] = { a1, a2, a3, a4, a5, a6, a7, a8 }; - IPAddressByteUnion temp; - for (int i = 0; i < 8; ++i) - { - temp.combined = array[i]; - address[i * 2] = temp.split[0]; - address[i * 2 + 1] = temp.split[1]; - } + setAddressGroup (address, i, array[i]); } IPAddress::IPAddress (uint32 n) noexcept @@ -180,19 +186,13 @@ IPAddress::IPAddress (const String& adr) { IPAddress v4Address (tokens[i]); - address[12] = v4Address.address[0]; - address[13] = v4Address.address[1]; - address[14] = v4Address.address[2]; - address[15] = v4Address.address[3]; + setAddressGroup (address, 6, static_cast ((v4Address.address[0] << 8) | v4Address.address[1])); + setAddressGroup (address, 7, static_cast ((v4Address.address[2] << 8) | v4Address.address[3])); break; } - IPAddressByteUnion temp; - temp.combined = CharacterFunctions::HexParser::parse (tokens[i].getCharPointer()); - - address[i * 2] = temp.split[0]; - address[i * 2 + 1] = temp.split[1]; + setAddressGroup (address, i, CharacterFunctions::HexParser::parse (tokens[i].getCharPointer())); } } } @@ -209,20 +209,10 @@ String IPAddress::toString() const return s; } - IPAddressByteUnion temp; - - temp.split[0] = address[0]; - temp.split[1] = address[1]; - - auto addressString = String::toHexString (temp.combined); + auto addressString = String::toHexString (getAddressGroup (address, 0)); for (int i = 1; i < 8; ++i) - { - temp.split[0] = address[i * 2]; - temp.split[1] = address[i * 2 + 1]; - - addressString << ':' << String::toHexString (temp.combined); - } + addressString << ':' << String::toHexString (getAddressGroup (address, i)); return getFormattedAddress (addressString); } @@ -366,11 +356,18 @@ bool IPAddress::isIPv4MappedAddress (const IPAddress& mappedAddress) IPAddress IPAddress::convertIPv4MappedAddressToIPv4 (const IPAddress& mappedAddress) { - // The address that you're converting needs to be IPv6! jassert (mappedAddress.isIPv6); if (isIPv4MappedAddress (mappedAddress)) - return { mappedAddress.address[12], mappedAddress.address[13], mappedAddress.address[14], mappedAddress.address[15] }; + { + const auto high = getAddressGroup (mappedAddress.address, 6); + const auto low = getAddressGroup (mappedAddress.address, 7); + + return { static_cast (high >> 8), + static_cast (high & 0xff), + static_cast (low >> 8), + static_cast (low & 0xff) }; + } return {}; } diff --git a/modules/yup_events/native/yup_MessageManager_mac.mm b/modules/yup_events/native/yup_MessageManager_mac.mm index 9e7ef4b96..ad8a96d47 100644 --- a/modules/yup_events/native/yup_MessageManager_mac.mm +++ b/modules/yup_events/native/yup_MessageManager_mac.mm @@ -423,11 +423,15 @@ static void shutdownNSApp() while (quitMessagePosted.get() == 0) { - if (runNSApplication(millisecondsToRunFor, quitMessagePosted)) + YUP_TRY { - if (loopCallback) - loopCallback(); + if (runNSApplication(millisecondsToRunFor, quitMessagePosted)) + { + if (loopCallback) + loopCallback(); + } } + YUP_CATCH_EXCEPTION } } @@ -465,7 +469,13 @@ static void shutdownNSApp() jassert(millisecondsToRunFor >= 0); jassert(isThisTheMessageThread()); // must only be called by the message thread - return runNSApplication(millisecondsToRunFor, quitMessagePosted); + YUP_TRY + { + return runNSApplication(millisecondsToRunFor, quitMessagePosted); + } + YUP_CATCH_EXCEPTION + + return quitMessagePosted.get() == 0; } #endif diff --git a/modules/yup_graphics/graphics/yup_Graphics.cpp b/modules/yup_graphics/graphics/yup_Graphics.cpp index d0e51c282..3dc74f295 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.cpp +++ b/modules/yup_graphics/graphics/yup_Graphics.cpp @@ -271,21 +271,24 @@ Graphics::Graphics (GraphicsContext& context, std::unique_ptr renderOptions.emplace_back(); currentRenderOptions().scale = 1.0f; - if (offscreenTarget == nullptr) - return; - - rive::gpu::RenderContext::FrameDescriptor frameDesc; - frameDesc.renderTargetWidth = static_cast (offscreenTarget->getWidth()); - frameDesc.renderTargetHeight = static_cast (offscreenTarget->getHeight()); - frameDesc.loadAction = rive::gpu::LoadAction::clear; - frameDesc.clearColor = clearColor; + beginOffscreenFrame ({ .clearColor = GpuColor (clearColor) }); +} - context.getGpuDevice()->beginOffscreen (*offscreenTarget, frameDesc); +Graphics::Graphics (GraphicsContext& context, RenderableTarget& target, uint32_t clearColor) noexcept + : context (context) + , offscreenTarget (std::addressof (target)) + , factory (*getOffscreenFactory (context, offscreenTarget)) + , ownedRenderer (makeOffscreenRenderer (context, offscreenTarget, target.getWidth(), target.getHeight())) + , renderer (*ownedRenderer) + , contextScale (1.0f) +{ + renderOptions.emplace_back(); + currentRenderOptions().scale = 1.0f; - currentRenderOptions().drawingArea = { 0.0f, 0.0f, static_cast (offscreenTarget->getWidth()), static_cast (offscreenTarget->getHeight()) }; + beginOffscreenFrame ({ .clearColor = GpuColor (clearColor) }); } -Graphics::Graphics (GraphicsContext& context, RenderableTarget& target, uint32_t clearColor) noexcept +Graphics::Graphics (GraphicsContext& context, RenderableTarget& target, const GpuFrameDescriptor& frameDesc) noexcept : context (context) , offscreenTarget (std::addressof (target)) , factory (*getOffscreenFactory (context, offscreenTarget)) @@ -296,13 +299,19 @@ Graphics::Graphics (GraphicsContext& context, RenderableTarget& target, uint32_t renderOptions.emplace_back(); currentRenderOptions().scale = 1.0f; - rive::gpu::RenderContext::FrameDescriptor frameDesc; - frameDesc.renderTargetWidth = static_cast (offscreenTarget->getWidth()); - frameDesc.renderTargetHeight = static_cast (offscreenTarget->getHeight()); - frameDesc.loadAction = rive::gpu::LoadAction::clear; - frameDesc.clearColor = clearColor; + beginOffscreenFrame (frameDesc); +} + +void Graphics::beginOffscreenFrame (const GpuFrameDescriptor& frameDesc) +{ + if (offscreenTarget == nullptr) + return; + + auto desc = frameDesc; + desc.renderTargetWidth = static_cast (offscreenTarget->getWidth()); + desc.renderTargetHeight = static_cast (offscreenTarget->getHeight()); - context.getGpuDevice()->beginOffscreen (*offscreenTarget, frameDesc); + context.getGpuDevice()->beginOffscreen (*offscreenTarget, desc); currentRenderOptions().drawingArea = { 0.0f, 0.0f, static_cast (offscreenTarget->getWidth()), static_cast (offscreenTarget->getHeight()) }; } diff --git a/modules/yup_graphics/graphics/yup_Graphics.h b/modules/yup_graphics/graphics/yup_Graphics.h index 727e5f0ab..e92d6dcc0 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.h +++ b/modules/yup_graphics/graphics/yup_Graphics.h @@ -156,6 +156,19 @@ class YUP_API Graphics */ Graphics (GraphicsContext& context, RenderableTarget& target, uint32_t clearColor = 0) noexcept; + /** Constructs a Graphics object rendering into an externally-owned renderable target, + with full control over the offscreen frame's msaa/dither/loadOp/clearColor. + + The target is not owned by this Graphics and must outlive it. Begins the + offscreen GPU frame immediately. Used by GpuCanvas, which owns the target. + + @param context Reference to the GraphicsContext to use for offscreen rendering. + @param target Reference to the externally-owned renderable target. + @param frameDesc Frame descriptor for the offscreen frame. Its renderTargetWidth/ + renderTargetHeight are ignored and overwritten from @p target. + */ + Graphics (GraphicsContext& context, RenderableTarget& target, const GpuFrameDescriptor& frameDesc) noexcept; + /** Finalizes an uncommitted offscreen frame without retaining its result. */ ~Graphics(); @@ -761,6 +774,8 @@ class YUP_API Graphics RenderOptions& currentRenderOptions(); const RenderOptions& currentRenderOptions() const; + void beginOffscreenFrame (const GpuFrameDescriptor& frameDesc); + void restoreState(); void clipPath (rive::RawPath& path); diff --git a/modules/yup_graphics/imaging/yup_Image.cpp b/modules/yup_graphics/imaging/yup_Image.cpp index 56d7554ec..f5909695b 100644 --- a/modules/yup_graphics/imaging/yup_Image.cpp +++ b/modules/yup_graphics/imaging/yup_Image.cpp @@ -74,16 +74,12 @@ bool Image::isValid() const noexcept //============================================================================== int Image::getWidth() const noexcept { - jassert (pixelData != nullptr); - - return pixelData->getWidth(); + return pixelData != nullptr ? pixelData->getWidth() : 0; } int Image::getHeight() const noexcept { - jassert (pixelData != nullptr); - - return pixelData->getHeight(); + return pixelData != nullptr ? pixelData->getHeight() : 0; } PixelFormat Image::getPixelFormat() const noexcept diff --git a/modules/yup_graphics/imaging/yup_Image.h b/modules/yup_graphics/imaging/yup_Image.h index 588de204d..b35a8af3a 100644 --- a/modules/yup_graphics/imaging/yup_Image.h +++ b/modules/yup_graphics/imaging/yup_Image.h @@ -74,10 +74,10 @@ class Image bool isValid() const noexcept; //============================================================================== - /** Returns the width of the image in pixels. */ + /** Returns the width of the image in pixels, or 0 if the image is invalid. */ int getWidth() const noexcept; - /** Returns the height of the image in pixels. */ + /** Returns the height of the image in pixels, or 0 if the image is invalid. */ int getHeight() const noexcept; /** Returns the pixel format of the image. */ diff --git a/modules/yup_graphics/imaging/yup_ImageFormatReader.cpp b/modules/yup_graphics/imaging/yup_ImageFormatReader.cpp index 1414da9e5..68c2bde35 100644 --- a/modules/yup_graphics/imaging/yup_ImageFormatReader.cpp +++ b/modules/yup_graphics/imaging/yup_ImageFormatReader.cpp @@ -22,17 +22,23 @@ namespace yup { -ImageFormatReader::ImageFormatReader (InputStream* sourceStream, const String& formatName_) +ImageFormatReader::ImageFormatReader (InputStream* sourceStream, const String& formatName_, bool deleteSourceWhenDestroyed_) : input (sourceStream) + , deleteSourceWhenDestroyed (deleteSourceWhenDestroyed_) , formatName (formatName_) { + if (! deleteSourceWhenDestroyed) + input.release(); } -ImageFormatReader::ImageFormatReader (InputStream* sourceStream, const String& formatName_, const ImageFormat::Options& opts) +ImageFormatReader::ImageFormatReader (InputStream* sourceStream, const String& formatName_, const ImageFormat::Options& opts, bool deleteSourceWhenDestroyed_) : input (sourceStream) + , deleteSourceWhenDestroyed (deleteSourceWhenDestroyed_) , formatName (formatName_) , options (opts) { + if (! deleteSourceWhenDestroyed) + input.release(); } Image ImageFormatReader::readFrame (int frameIndex) diff --git a/modules/yup_graphics/imaging/yup_ImageFormatReader.h b/modules/yup_graphics/imaging/yup_ImageFormatReader.h index 009d43e51..7b55d00f7 100644 --- a/modules/yup_graphics/imaging/yup_ImageFormatReader.h +++ b/modules/yup_graphics/imaging/yup_ImageFormatReader.h @@ -118,18 +118,41 @@ class YUP_API ImageFormatReader /** The pixel format of the decoded image. */ PixelFormat pixelFormat = PixelFormat::RGBA; - /** The input stream, for use by subclasses. */ + /** The input stream, for use by subclasses. + + Null when this reader was created with deleteSourceWhenDestroyed set to false, + because it then keeps no stream of its own. + */ std::unique_ptr input; /** Metadata extracted from the image file (nullptr if no metadata was requested or found). */ ImageMetadata::Ptr metadata; + /** Whether this reader owns the source stream and deletes it when destroyed. */ + bool deleteSourceWhenDestroyed = true; + protected: - /** Creates an ImageFormatReader and takes ownership of the source stream. */ - ImageFormatReader (InputStream* sourceStream, const String& formatName); + /** Creates an ImageFormatReader. + + @param sourceStream The stream to read from + @param formatName The name reported by getFormatName() + @param deleteSourceWhenDestroyed Whether to delete @p sourceStream when this + reader is destroyed. Pass true to take ownership, + which is what every built-in format and every + reader created through ImageFormatManager wants. + Pass false when the caller keeps ownership; the + reader then holds no stream at all and must not + need one after construction. + */ + ImageFormatReader (InputStream* sourceStream, + const String& formatName, + bool deleteSourceWhenDestroyed = true); /** Creates an ImageFormatReader with options. */ - ImageFormatReader (InputStream* sourceStream, const String& formatName, const ImageFormat::Options& opts); + ImageFormatReader (InputStream* sourceStream, + const String& formatName, + const ImageFormat::Options& opts, + bool deleteSourceWhenDestroyed = true); private: String formatName; diff --git a/modules/yup_graphics/imaging/yup_ImageFormatWriter.cpp b/modules/yup_graphics/imaging/yup_ImageFormatWriter.cpp index d49af7990..493b10249 100644 --- a/modules/yup_graphics/imaging/yup_ImageFormatWriter.cpp +++ b/modules/yup_graphics/imaging/yup_ImageFormatWriter.cpp @@ -24,11 +24,15 @@ namespace yup ImageFormatWriter::ImageFormatWriter (OutputStream* destStream, const String& formatName_, - PixelFormat pixelFormat_) + PixelFormat pixelFormat_, + bool deleteSourceWhenDestroyed_) : output (destStream) + , deleteSourceWhenDestroyed (deleteSourceWhenDestroyed_) , formatName (formatName_) , pixelFormat (pixelFormat_) { + if (! deleteSourceWhenDestroyed) + output.release(); } ImageFormatWriter::~ImageFormatWriter() diff --git a/modules/yup_graphics/imaging/yup_ImageFormatWriter.h b/modules/yup_graphics/imaging/yup_ImageFormatWriter.h index 9a075bb18..d470fd849 100644 --- a/modules/yup_graphics/imaging/yup_ImageFormatWriter.h +++ b/modules/yup_graphics/imaging/yup_ImageFormatWriter.h @@ -103,12 +103,33 @@ class YUP_API ImageFormatWriter */ virtual bool endAnimation(); - /** The output stream, for use by subclasses. */ + /** The output stream, for use by subclasses. + + Null when this writer was created with deleteSourceWhenDestroyed set to false, + because it then keeps no stream of its own. + */ std::unique_ptr output; + /** Whether this writer owns the destination stream and deletes it when destroyed. */ + bool deleteSourceWhenDestroyed = true; + protected: - /** Creates an ImageFormatWriter and takes ownership of the destination stream. */ - ImageFormatWriter (OutputStream* destStream, const String& formatName, PixelFormat pixelFormat); + /** Creates an ImageFormatWriter. + + @param destStream The stream to write to + @param formatName The name reported by getFormatName() + @param pixelFormat The pixel format of the data being written + @param deleteSourceWhenDestroyed Whether to delete @p destStream when this writer + is destroyed. Pass true to take ownership, which is + what every built-in format and every writer created + through ImageFormatManager wants. Pass false when the + caller keeps ownership; the writer then holds no + stream at all and must not need one after construction. + */ + ImageFormatWriter (OutputStream* destStream, + const String& formatName, + PixelFormat pixelFormat, + bool deleteSourceWhenDestroyed = true); private: String formatName; diff --git a/modules/yup_graphics/rhi/yup_GpuCanvas.cpp b/modules/yup_graphics/rhi/yup_GpuCanvas.cpp index 2704c55be..cbda10d09 100644 --- a/modules/yup_graphics/rhi/yup_GpuCanvas.cpp +++ b/modules/yup_graphics/rhi/yup_GpuCanvas.cpp @@ -71,7 +71,7 @@ int GpuCanvas::getHeight() const noexcept //============================================================================== -Graphics& GpuCanvas::beginDraw() +Graphics& GpuCanvas::beginDraw (const GpuFrameDescriptor& frameDesc) { jassert (context != nullptr && target != nullptr); @@ -81,7 +81,7 @@ Graphics& GpuCanvas::beginDraw() target->invalidateCachedTexture(); - graphics = std::make_unique (*context, *target->getRenderableTarget(), 0u); + graphics = std::make_unique (*context, *target->getRenderableTarget(), frameDesc); frameOpen = true; return *graphics; diff --git a/modules/yup_graphics/rhi/yup_GpuCanvas.h b/modules/yup_graphics/rhi/yup_GpuCanvas.h index 439b24080..e5676addb 100644 --- a/modules/yup_graphics/rhi/yup_GpuCanvas.h +++ b/modules/yup_graphics/rhi/yup_GpuCanvas.h @@ -132,8 +132,13 @@ class YUP_API GpuCanvas : public ReferenceCountedObject resource reallocation. Not applicable to canvases used only via beginRenderPass(). + + @param frameDesc Controls msaa/dither/loadOp/clearColor for the offscreen + frame. Its renderTargetWidth/renderTargetHeight are ignored + and auto-filled from the canvas. Defaults reproduce the + previous hardcoded behaviour (clear to transparent black). */ - Graphics& beginDraw(); + Graphics& beginDraw (const GpuFrameDescriptor& frameDesc = {}); /** Finalises any open 2D GPU render command. diff --git a/modules/yup_gui/component/yup_Component.cpp b/modules/yup_gui/component/yup_Component.cpp index 0405b87ff..0d4a4cc12 100644 --- a/modules/yup_gui/component/yup_Component.cpp +++ b/modules/yup_gui/component/yup_Component.cpp @@ -499,12 +499,16 @@ void Component::contentScaleChanged ([[maybe_unused]] float dpiScale) {} void Component::setOpacity (float newOpacity) { - newOpacity = jlimit (0.0f, 1.0f, newOpacity); + auto clampedOpacity = static_cast (jlimit (0.0f, 1.0f, newOpacity) * 255); + if (opacity == clampedOpacity) + return; - opacity = static_cast (newOpacity * 255); + opacity = clampedOpacity; if (options.onDesktop && native != nullptr) native->setOpacity (newOpacity); + + opacityChanged(); } float Component::getOpacity() const @@ -512,6 +516,8 @@ float Component::getOpacity() const return opacity / 255.0f; } +void Component::opacityChanged() {} + //============================================================================== bool Component::isOpaque() const @@ -1206,8 +1212,6 @@ std::optional Component::findMetric (const Identifier& metricId) const //============================================================================== -//============================================================================== - void Component::setComponentEffect (ComponentEffect::Ptr effect) { componentEffect = std::move (effect); @@ -1426,6 +1430,11 @@ void Component::paintSubtree (Graphics& g, const Rectangle& drawingArea, { isRepainting.store (true, std::memory_order_relaxed); + const ErasedScopeGuard clearRepaintingFlag ([this] + { + isRepainting.store (false, std::memory_order_relaxed); + }); + { const bool shouldMeasurePaint = ! options.paintProfilingDisabled && ! componentListeners.isEmpty(); @@ -1499,8 +1508,6 @@ void Component::paintSubtree (Graphics& g, const Rectangle& drawingArea, paintChildrenAndOverChildren (g, clipArea, renderContinuous); } } - - isRepainting.store (false, std::memory_order_relaxed); } //============================================================================== diff --git a/modules/yup_gui/component/yup_Component.h b/modules/yup_gui/component/yup_Component.h index c79f37785..4b1af4a99 100644 --- a/modules/yup_gui/component/yup_Component.h +++ b/modules/yup_gui/component/yup_Component.h @@ -100,7 +100,7 @@ class YUP_API Component : public MouseListener @param shouldBeVisible True if the component should be visible, false otherwise. */ - void setVisible (bool shouldBeVisible); + virtual void setVisible (bool shouldBeVisible); /** Check if the component is showing. @@ -129,7 +129,7 @@ class YUP_API Component : public MouseListener @param title The new title of the component. */ - void setTitle (const String& title); + virtual void setTitle (const String& title); //============================================================================== /** @@ -618,6 +618,11 @@ class YUP_API Component : public MouseListener */ void setOpacity (float opacity); + /** + Called when the opacity of the component changes. + */ + virtual void opacityChanged(); + //============================================================================== /** Check if the component is opaque. diff --git a/modules/yup_gui/native/yup_Windowing_sdl.cpp b/modules/yup_gui/native/yup_Windowing_sdl.cpp index 3f26f61ef..bf3366c19 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl.cpp @@ -890,10 +890,14 @@ void SDLComponentNative::run() if (threadShouldExit()) break; - YUP_AUTORELEASEPOOL + YUP_TRY { - renderFrame(); + YUP_AUTORELEASEPOOL + { + renderFrame(); + } } + YUP_CATCH_EXCEPTION if (threadShouldExit()) break; @@ -1006,6 +1010,7 @@ bool SDLComponentNative::renderFrame() const bool isGL = currentGraphicsApi == GpuPlatform::OpenGL || currentGraphicsApi == GpuPlatform::OpenGLES; bool glContextLocked = false; + bool frameBegun = false; auto renderInternal = [&]() -> bool { @@ -1118,6 +1123,7 @@ bool SDLComponentNative::renderFrame() YUP_PROFILE_NAMED_INTERNAL_TRACE (ContextBegin); context->begin (frameDescriptor); + frameBegun = true; } // Repaint the component hierarchy (runs user paint() under the lock). @@ -1153,8 +1159,25 @@ bool SDLComponentNative::renderFrame() return true; }; - auto unlockGLContextAtExit = ErasedScopeGuard ([&] + auto endFrameAtExit = ErasedScopeGuard ([&] { + if (frameBegun) + { + { + YUP_PROFILE_NAMED_INTERNAL_TRACE (ContextEnd); + + context->end (getNativeHandle()); + context->tick(); + } + + if (isGL && window != nullptr) + { + YUP_PROFILE_NAMED_INTERNAL_TRACE (SwapWindow); + + SDL_GL_SwapWindow (window); + } + } + if constexpr (! renderDrivenByTimer) { if (glContextLocked) @@ -1186,20 +1209,6 @@ bool SDLComponentNative::renderFrame() } } - { - YUP_PROFILE_NAMED_INTERNAL_TRACE (ContextEnd); - - context->end (getNativeHandle()); - context->tick(); - } - - if (isGL && window != nullptr) - { - YUP_PROFILE_NAMED_INTERNAL_TRACE (SwapWindow); - - SDL_GL_SwapWindow (window); - } - return true; } @@ -1247,8 +1256,8 @@ void SDLComponentNative::stopRendering() if (isThreadRunning()) { signalThreadShouldExit(); - notify(); renderEvent.signal(); + notify(); stopThread (-1); YUP_MODULE_DBG (GUI_WINDOWING, "SDL: stopped render thread"); } @@ -2538,10 +2547,14 @@ bool SDLComponentNative::eventDispatcher (void* userdata, SDL_Event* event) if (auto component = Desktop::getInstance()->getNativeComponent (userdata)) { - if (auto nativeComponent = dynamic_cast (component.get())) - nativeComponent->handleEvent (event); - else - YUP_MODULE_DBG (GUI_WINDOWING, "Received event for unknown native component"); + YUP_TRY + { + if (auto nativeComponent = dynamic_cast (component.get())) + nativeComponent->handleEvent (event); + else + YUP_MODULE_DBG (GUI_WINDOWING, "Received event for unknown native component"); + } + YUP_CATCH_EXCEPTION } return true; diff --git a/modules/yup_python/bindings/yup_YupAudioBasics_bindings.cpp b/modules/yup_python/bindings/yup_YupAudioBasics_bindings.cpp index d70d2503b..1dba269cd 100644 --- a/modules/yup_python/bindings/yup_YupAudioBasics_bindings.cpp +++ b/modules/yup_python/bindings/yup_YupAudioBasics_bindings.cpp @@ -101,9 +101,20 @@ void registerYupAudioBasicsBindings (py::module_& m) registerAudioBuffer.operator() (m, "AudioBufferFloat"); registerAudioBuffer.operator() (m, "AudioBufferDouble"); - // Alias for the most common type + // Alias for the most common type, subscriptable the way the other templated + // types are, so `yup.AudioBuffer[float]` works as well as `yup.AudioBuffer`. + // Python has a single floating-point type, so float is the only key this alias + // can carry and the double specialization stays reachable as AudioBufferDouble. m.attr ("AudioBuffer") = m.attr ("AudioBufferFloat"); + m.attr ("AudioBuffer").attr ("__class_getitem__") = py::cpp_function ([] (const py::object& keyType) -> py::object + { + if (! py::isinstance (keyType) || ! py::type::of (py::float_ (0.0)).is (keyType)) + throw py::type_error ("AudioBuffer[] takes float, the only floating-point type in Python"); + + return py::module_::import (PythonModuleName).attr ("AudioBufferFloat"); + }); + // ============================================================================================ yup::AudioChannelSet (forward declare for Array) py::class_ classAudioChannelSet (m, "AudioChannelSet"); @@ -473,7 +484,7 @@ void registerYupAudioBasicsBindings (py::module_& m) // ============================================================================================ yup::PositionableAudioSource - py::class_> classPositionableAudioSource (m, "PositionableAudioSource"); + py::class_> classPositionableAudioSource (m, "PositionableAudioSource"); classPositionableAudioSource .def (py::init<>()) diff --git a/modules/yup_python/bindings/yup_YupAudioDevices_bindings.cpp b/modules/yup_python/bindings/yup_YupAudioDevices_bindings.cpp new file mode 100644 index 000000000..7c1c04ac0 --- /dev/null +++ b/modules/yup_python/bindings/yup_YupAudioDevices_bindings.cpp @@ -0,0 +1,257 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include "yup_YupAudioDevices_bindings.h" + +#include "../utilities/yup_PythonInterop.h" + +#define YUP_PYTHON_INCLUDE_PYBIND11_OPERATORS +#define YUP_PYTHON_INCLUDE_PYBIND11_FUNCTIONAL +#include "../utilities/yup_PyBind11Includes.h" + +//============================================================================== + +namespace yup::Bindings +{ + +namespace py = pybind11; +using namespace py::literals; + +void registerYupAudioDevicesBindings (py::module_& m) +{ + // clang-format off + + // ============================================================================================ yup::WASAPIDeviceMode + + py::enum_ (m, "WASAPIDeviceMode") + .value ("shared", WASAPIDeviceMode::shared) + .value ("exclusive", WASAPIDeviceMode::exclusive) + .value ("sharedLowLatency", WASAPIDeviceMode::sharedLowLatency) + .export_values(); + + // ============================================================================================ yup::AudioIODeviceCallbackContext + + py::class_ (m, "AudioIODeviceCallbackContext") + .def (py::init<>()) + .def_readwrite ("hostTimeNs", &AudioIODeviceCallbackContext::hostTimeNs); + + // ============================================================================================ yup::AudioIODeviceCallback + + py::class_ (m, "AudioIODeviceCallback") + .def (py::init<>()) + .def ("audioDeviceAboutToStart", &AudioIODeviceCallback::audioDeviceAboutToStart) + .def ("audioDeviceStopped", &AudioIODeviceCallback::audioDeviceStopped) + .def ("audioDeviceError", &AudioIODeviceCallback::audioDeviceError); + + // ============================================================================================ yup::AudioIODevice + + py::class_ (m, "AudioIODevice") + .def ("getName", &AudioIODevice::getName) + .def ("getTypeName", &AudioIODevice::getTypeName) + .def ("getOutputChannelNames", &AudioIODevice::getOutputChannelNames) + .def ("getInputChannelNames", &AudioIODevice::getInputChannelNames) + .def ("getDefaultOutputChannels", &AudioIODevice::getDefaultOutputChannels) + .def ("getDefaultInputChannels", &AudioIODevice::getDefaultInputChannels) + .def ("getAvailableSampleRates", &AudioIODevice::getAvailableSampleRates) + .def ("getAvailableBufferSizes", &AudioIODevice::getAvailableBufferSizes) + .def ("getDefaultBufferSize", &AudioIODevice::getDefaultBufferSize) + .def ("open", &AudioIODevice::open) + .def ("close", &AudioIODevice::close) + .def ("isOpen", &AudioIODevice::isOpen) + .def ("start", &AudioIODevice::start) + .def ("stop", &AudioIODevice::stop) + .def ("isPlaying", &AudioIODevice::isPlaying) + .def ("getLastError", &AudioIODevice::getLastError) + .def ("getCurrentBufferSizeSamples", &AudioIODevice::getCurrentBufferSizeSamples) + .def ("getCurrentSampleRate", &AudioIODevice::getCurrentSampleRate) + .def ("getCurrentBitDepth", &AudioIODevice::getCurrentBitDepth) + .def ("getActiveOutputChannels", &AudioIODevice::getActiveOutputChannels) + .def ("getActiveInputChannels", &AudioIODevice::getActiveInputChannels) + .def ("getOutputLatencyInSamples", &AudioIODevice::getOutputLatencyInSamples) + .def ("getInputLatencyInSamples", &AudioIODevice::getInputLatencyInSamples) + .def ("hasControlPanel", &AudioIODevice::hasControlPanel) + .def ("showControlPanel", &AudioIODevice::showControlPanel) + .def ("setAudioPreprocessingEnabled", &AudioIODevice::setAudioPreprocessingEnabled) + .def ("getXRunCount", &AudioIODevice::getXRunCount) + .def ("__repr__", [] (const AudioIODevice& self) + { + String result; + result + << "<" << Helpers::pythonizeModuleClassName (PythonModuleName, typeid (self).name(), 1) + << " name=\"" << self.getName() << "\"" + << " type=\"" << self.getTypeName() << "\">"; + return result; + }); + + // ============================================================================================ yup::AudioIODeviceType + + py::class_ (m, "AudioIODeviceType") + .def ("getTypeName", &AudioIODeviceType::getTypeName) + .def ("scanForDevices", &AudioIODeviceType::scanForDevices) + .def ("getDeviceNames", &AudioIODeviceType::getDeviceNames, "wantInputNames"_a = false) + .def ("getDefaultDeviceIndex", &AudioIODeviceType::getDefaultDeviceIndex, "forInput"_a) + .def ("getIndexOfDevice", &AudioIODeviceType::getIndexOfDevice, "device"_a, "asInput"_a) + .def ("hasSeparateInputsAndOutputs", &AudioIODeviceType::hasSeparateInputsAndOutputs) + .def ("createDevice", &AudioIODeviceType::createDevice, + "outputDeviceName"_a, "inputDeviceName"_a, + py::return_value_policy::take_ownership) + .def ("__repr__", [] (const AudioIODeviceType& self) + { + String result; + result + << "<" << Helpers::pythonizeModuleClassName (PythonModuleName, typeid (self).name(), 1) + << " typeName=\"" << self.getTypeName() << "\">"; + return result; + }); + + // ============================================================================================ yup::AudioDeviceManager::AudioDeviceSetup + + py::class_ (m, "AudioDeviceSetup") + .def (py::init<>()) + .def_readwrite ("outputDeviceName", &AudioDeviceManager::AudioDeviceSetup::outputDeviceName) + .def_readwrite ("inputDeviceName", &AudioDeviceManager::AudioDeviceSetup::inputDeviceName) + .def_readwrite ("sampleRate", &AudioDeviceManager::AudioDeviceSetup::sampleRate) + .def_readwrite ("bufferSize", &AudioDeviceManager::AudioDeviceSetup::bufferSize) + .def_readwrite ("inputChannels", &AudioDeviceManager::AudioDeviceSetup::inputChannels) + .def_readwrite ("useDefaultInputChannels", &AudioDeviceManager::AudioDeviceSetup::useDefaultInputChannels) + .def_readwrite ("outputChannels", &AudioDeviceManager::AudioDeviceSetup::outputChannels) + .def_readwrite ("useDefaultOutputChannels", &AudioDeviceManager::AudioDeviceSetup::useDefaultOutputChannels) + .def ("__eq__", &AudioDeviceManager::AudioDeviceSetup::operator==) + .def ("__ne__", &AudioDeviceManager::AudioDeviceSetup::operator!=); + + // ============================================================================================ yup::AudioDeviceManager::LevelMeter + + py::class_> (m, "LevelMeter") + .def ("getCurrentLevel", &AudioDeviceManager::LevelMeter::getCurrentLevel); + + // ============================================================================================ yup::AudioDeviceManager + + py::class_ (m, "AudioDeviceManager") + .def (py::init<>()) + .def ("initialise", [] (AudioDeviceManager& self, + int numInputChannelsNeeded, + int numOutputChannelsNeeded, + const XmlElement* savedState, + bool selectDefaultDeviceOnFailure, + const String& preferredDefaultDeviceName, + const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions) + { + return self.initialise (numInputChannelsNeeded, + numOutputChannelsNeeded, + savedState, + selectDefaultDeviceOnFailure, + preferredDefaultDeviceName, + preferredSetupOptions); + }, + "numInputChannelsNeeded"_a, + "numOutputChannelsNeeded"_a, + "savedState"_a = nullptr, + "selectDefaultDeviceOnFailure"_a = true, + "preferredDefaultDeviceName"_a = String(), + "preferredSetupOptions"_a = nullptr) + .def ("initialiseWithDefaultDevices", &AudioDeviceManager::initialiseWithDefaultDevices, + "numInputChannelsNeeded"_a, "numOutputChannelsNeeded"_a) + .def ("createStateXml", &AudioDeviceManager::createStateXml) + .def ("getAudioDeviceSetup", [] (AudioDeviceManager& self) + { + return self.getAudioDeviceSetup(); + }) + .def ("setAudioDeviceSetup", &AudioDeviceManager::setAudioDeviceSetup, + "newSetup"_a, "treatAsChosenDevice"_a) + .def ("getCurrentAudioDevice", &AudioDeviceManager::getCurrentAudioDevice, + py::return_value_policy::reference) + .def ("getCurrentAudioDeviceType", &AudioDeviceManager::getCurrentAudioDeviceType) + .def ("getCurrentDeviceTypeObject", &AudioDeviceManager::getCurrentDeviceTypeObject, + py::return_value_policy::reference) + .def ("setCurrentAudioDeviceType", &AudioDeviceManager::setCurrentAudioDeviceType, + "type"_a, "treatAsChosenDevice"_a) + .def ("getAvailableDeviceTypes", [] (AudioDeviceManager& self) + { + auto& deviceTypes = self.getAvailableDeviceTypes(); + + py::list result; + + for (int i = 0; i < deviceTypes.size(); ++i) + result.append (py::cast (deviceTypes[i], py::return_value_policy::reference)); + + return result; + }) + .def ("closeAudioDevice", &AudioDeviceManager::closeAudioDevice) + .def ("restartLastAudioDevice", &AudioDeviceManager::restartLastAudioDevice) + .def ("addAudioCallback", &AudioDeviceManager::addAudioCallback) + .def ("removeAudioCallback", &AudioDeviceManager::removeAudioCallback) + .def ("getCpuUsage", &AudioDeviceManager::getCpuUsage) + .def ("playTestSound", &AudioDeviceManager::playTestSound) + .def ("getInputLevelGetter", &AudioDeviceManager::getInputLevelGetter) + .def ("getOutputLevelGetter", &AudioDeviceManager::getOutputLevelGetter) + .def ("__repr__", [] (const AudioDeviceManager& self) + { + String result; + result + << "<" << Helpers::pythonizeModuleClassName (PythonModuleName, typeid (self).name(), 1) + << " type=\"" << self.getCurrentAudioDeviceType() << "\">"; + return result; + }); + + // ============================================================================================ yup::AudioSourcePlayer + + py::class_ (m, "AudioSourcePlayer") + .def (py::init<>()) + .def ("setSource", [] (AudioSourcePlayer& self, AudioSource* source) + { + self.setSource (source); + }, "newSource"_a, py::keep_alive<1, 2>()) + .def ("getCurrentSource", [] (AudioSourcePlayer& self) -> py::object + { + auto* src = self.getCurrentSource(); + if (src == nullptr) + return py::none(); + return py::cast (src, py::return_value_policy::reference); + }) + .def ("setGain", &AudioSourcePlayer::setGain) + .def ("getGain", &AudioSourcePlayer::getGain) + .def ("prepareToPlay", &AudioSourcePlayer::prepareToPlay); + + // ============================================================================================ yup::AudioTransportSource + + py::class_ (m, "AudioTransportSource") + .def (py::init<>()) + .def ("setSource", &AudioTransportSource::setSource, + "newSource"_a, + "readAheadBufferSize"_a = 0, + "readAheadThread"_a = nullptr, + "sourceSampleRateToCorrectFor"_a = 0.0, + "maxNumChannels"_a = 2, + py::keep_alive<1, 2>()) + .def ("setPosition", &AudioTransportSource::setPosition) + .def ("getCurrentPosition", &AudioTransportSource::getCurrentPosition) + .def ("getLengthInSeconds", &AudioTransportSource::getLengthInSeconds) + .def ("hasStreamFinished", &AudioTransportSource::hasStreamFinished) + .def ("start", &AudioTransportSource::start) + .def ("stop", &AudioTransportSource::stop) + .def ("isPlaying", &AudioTransportSource::isPlaying) + .def ("setGain", &AudioTransportSource::setGain) + .def ("getGain", &AudioTransportSource::getGain); + + // clang-format on +} + +} // namespace yup::Bindings diff --git a/modules/yup_python/bindings/yup_YupAudioDevices_bindings.h b/modules/yup_python/bindings/yup_YupAudioDevices_bindings.h new file mode 100644 index 000000000..97cc48c63 --- /dev/null +++ b/modules/yup_python/bindings/yup_YupAudioDevices_bindings.h @@ -0,0 +1,80 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#if ! YUP_MODULE_AVAILABLE_yup_audio_devices +#error This binding file requires adding the yup_audio_devices module in the project +#else +#include +#endif + +#include "yup_YupCore_bindings.h" +#include "yup_YupEvents_bindings.h" +#include "yup_YupAudioBasics_bindings.h" + +#define YUP_PYTHON_INCLUDE_PYBIND11_OPERATORS +#define YUP_PYTHON_INCLUDE_PYBIND11_STL +#include "../utilities/yup_PyBind11Includes.h" + +namespace yup::Bindings +{ + +//============================================================================== + +void registerYupAudioDevicesBindings (pybind11::module_& m); + +//============================================================================== + +struct PyAudioIODeviceCallback : AudioIODeviceCallback +{ + // NOTE: audioDeviceIOCallbackWithContext uses raw float* pointer arrays + // that pybind11 cannot marshal. Python subclasses should use AudioSource + + // AudioSourcePlayer for custom audio processing instead. This method falls + // through to the C++ default (no-op). + void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, + int numInputChannels, + float* const* outputChannelData, + int numOutputChannels, + int numSamples, + const AudioIODeviceCallbackContext& context) override + { + AudioIODeviceCallback::audioDeviceIOCallbackWithContext ( + inputChannelData, numInputChannels, outputChannelData, numOutputChannels, numSamples, context); + } + + void audioDeviceAboutToStart (AudioIODevice* device) override + { + PYBIND11_OVERRIDE_PURE (void, AudioIODeviceCallback, audioDeviceAboutToStart, device); + } + + void audioDeviceStopped() override + { + PYBIND11_OVERRIDE_PURE (void, AudioIODeviceCallback, audioDeviceStopped); + } + + void audioDeviceError (const String& errorMessage) override + { + PYBIND11_OVERRIDE (void, AudioIODeviceCallback, audioDeviceError, errorMessage); + } +}; + +} // namespace yup::Bindings diff --git a/modules/yup_python/bindings/yup_YupAudioFormats_bindings.cpp b/modules/yup_python/bindings/yup_YupAudioFormats_bindings.cpp new file mode 100644 index 000000000..a7421ee1d --- /dev/null +++ b/modules/yup_python/bindings/yup_YupAudioFormats_bindings.cpp @@ -0,0 +1,138 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include "yup_YupAudioFormats_bindings.h" + +#include "../utilities/yup_PythonInterop.h" + +#define YUP_PYTHON_INCLUDE_PYBIND11_OPERATORS +#define YUP_PYTHON_INCLUDE_PYBIND11_FUNCTIONAL +#include "../utilities/yup_PyBind11Includes.h" + +//============================================================================== + +namespace yup::Bindings +{ + +namespace py = pybind11; +using namespace py::literals; + +void registerYupAudioFormatsBindings (py::module_& m) +{ + // clang-format off + + // ============================================================================================ yup::AudioFormatType + + py::enum_ (m, "AudioFormatType") + .value ("wav", AudioFormatType::wav) + .value ("mp3", AudioFormatType::mp3) + .value ("flac", AudioFormatType::flac) + .value ("ogg", AudioFormatType::ogg) + .value ("opus", AudioFormatType::opus) + .value ("coreAudio", AudioFormatType::coreAudio) + .value ("windowsMedia", AudioFormatType::windowsMedia) + .value ("all", AudioFormatType::all) + .export_values(); + + // ============================================================================================ yup::AudioFormatManager + + py::class_ (m, "AudioFormatManager") + .def (py::init<>()) + .def ("registerDefaultFormats", &AudioFormatManager::registerDefaultFormats, + "types"_a = AudioFormatType::all) + .def ("registerFormat", &AudioFormatManager::registerFormat) + .def ("createReaderFor", &AudioFormatManager::createReaderFor) + .def ("__repr__", [] (const AudioFormatManager& self) + { + String result; + result + << "<" << Helpers::pythonizeModuleClassName (PythonModuleName, typeid (self).name(), 1) + << " object at " << String::formatted ("%p", std::addressof (self)) << ">"; + return result; + }); + + // ============================================================================================ yup::AudioFormatReader + + py::class_ (m, "AudioFormatReader") + .def ("getFormatName", &AudioFormatReader::getFormatName) + .def ("read", [](AudioFormatReader& self, + AudioBuffer* buffer, + int startSampleInDestBuffer, + int numSamples, + int64 readerStartSample, + bool useReaderLeftChan, + bool useReaderRightChan) + { + return self.read (buffer, startSampleInDestBuffer, numSamples, + readerStartSample, useReaderLeftChan, useReaderRightChan); + }, + "buffer"_a, + "startSampleInDestBuffer"_a, + "numSamples"_a, + "readerStartSample"_a, + "useReaderLeftChan"_a, + "useReaderRightChan"_a) + .def_readonly ("sampleRate", &AudioFormatReader::sampleRate) + .def_readonly ("bitsPerSample", &AudioFormatReader::bitsPerSample) + .def_readonly ("lengthInSamples", &AudioFormatReader::lengthInSamples) + .def_readonly ("numChannels", &AudioFormatReader::numChannels) + .def_readonly ("usesFloatingPointData", &AudioFormatReader::usesFloatingPointData) + .def_readonly ("metadataValues", &AudioFormatReader::metadataValues) + .def ("__repr__", [] (const AudioFormatReader& self) + { + String result; + result + << "<" << Helpers::pythonizeModuleClassName (PythonModuleName, typeid (self).name(), 1) + << " format=\"" << self.getFormatName() << "\"" + << " sampleRate=" << self.sampleRate + << " numChannels=" << self.numChannels + << " lengthInSamples=" << self.lengthInSamples << ">"; + return result; + }); + + // ============================================================================================ yup::AudioFormatReaderSource + + py::class_ (m, "AudioFormatReaderSource") + .def (py::init ([] (AudioFormatReader* reader) + { + return std::make_unique (reader, false); + }), + "sourceReader"_a, py::keep_alive<1, 2>()) + .def ("getAudioFormatReader", [] (AudioFormatReaderSource& self) -> AudioFormatReader* + { + if (auto* reader = self.getAudioFormatReader()) + return reader; + return nullptr; + }, py::return_value_policy::reference) + .def ("__repr__", [] (const AudioFormatReaderSource& self) + { + String result; + result + << "<" << Helpers::pythonizeModuleClassName (PythonModuleName, typeid (self).name(), 1) + << " totalLength=" << self.getTotalLength() + << " looping=" << (self.isLooping() ? "true" : "false") << ">"; + return result; + }); + + // clang-format on +} + +} // namespace yup::Bindings diff --git a/modules/yup_python/bindings/yup_YupAudioFormats_bindings.h b/modules/yup_python/bindings/yup_YupAudioFormats_bindings.h new file mode 100644 index 000000000..2d9b6a921 --- /dev/null +++ b/modules/yup_python/bindings/yup_YupAudioFormats_bindings.h @@ -0,0 +1,44 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#if ! YUP_MODULE_AVAILABLE_yup_audio_formats +#error This binding file requires adding the yup_audio_formats module in the project +#else +#include +#endif + +#include "yup_YupCore_bindings.h" +#include "yup_YupAudioBasics_bindings.h" + +#define YUP_PYTHON_INCLUDE_PYBIND11_OPERATORS +#define YUP_PYTHON_INCLUDE_PYBIND11_STL +#include "../utilities/yup_PyBind11Includes.h" + +namespace yup::Bindings +{ + +//============================================================================== + +void registerYupAudioFormatsBindings (pybind11::module_& m); + +} // namespace yup::Bindings diff --git a/modules/yup_python/bindings/yup_YupCore_bindings.cpp b/modules/yup_python/bindings/yup_YupCore_bindings.cpp index 4b7f45c94..ac13b2537 100644 --- a/modules/yup_python/bindings/yup_YupCore_bindings.cpp +++ b/modules/yup_python/bindings/yup_YupCore_bindings.cpp @@ -1473,7 +1473,7 @@ void registerYupCoreBindings (py::module_& m) // ============================================================================================ yup::InputStream - py::class_> classInputStream (m, "InputStream"); + py::class_, py::smart_holder> classInputStream (m, "InputStream"); classInputStream .def (py::init<>()) @@ -1506,13 +1506,13 @@ void registerYupCoreBindings (py::module_& m) .def ("setPosition", &InputStream::setPosition, "pos"_a) .def ("skipNextBytes", &InputStream::skipNextBytes, "numBytesToSkip"_a); - py::class_> classBufferedInputStream (m, "BufferedInputStream"); + py::class_, py::smart_holder> classBufferedInputStream (m, "BufferedInputStream"); classBufferedInputStream .def (py::init(), "sourceStream"_a, "bufferSize"_a) .def ("peekByte", &BufferedInputStream::peekByte); - py::class_> classMemoryInputStream (m, "MemoryInputStream"); + py::class_, py::smart_holder> classMemoryInputStream (m, "MemoryInputStream"); classMemoryInputStream .def (py::init(), "data"_a, "keepInternalCopyOfData"_a) @@ -1527,7 +1527,7 @@ void registerYupCoreBindings (py::module_& m) }, py::return_value_policy::reference_internal) .def ("getDataSize", &MemoryInputStream::getDataSize); - py::class_> classSubregionStream (m, "SubregionStream"); + py::class_, py::smart_holder> classSubregionStream (m, "SubregionStream"); classSubregionStream .def (py::init(), @@ -1536,7 +1536,7 @@ void registerYupCoreBindings (py::module_& m) "lengthOfSourceStream"_a, "deleteSourceWhenDestroyed"_a); - py::class_> classGZIPDecompressorInputStream (m, "GZIPDecompressorInputStream"); + py::class_, py::smart_holder> classGZIPDecompressorInputStream (m, "GZIPDecompressorInputStream"); py::enum_ (classGZIPDecompressorInputStream, "Format") .value ("zlibFormat", GZIPDecompressorInputStream::Format::zlibFormat) @@ -1554,7 +1554,7 @@ void registerYupCoreBindings (py::module_& m) // ============================================================================================ yup::InputSource - py::class_> classInputSource (m, "InputSource"); + py::class_, py::smart_holder> classInputSource (m, "InputSource"); classInputSource .def (py::init<>()) @@ -1812,7 +1812,7 @@ void registerYupCoreBindings (py::module_& m) // ============================================================================================ yup::File*Stream - py::class_> classFileInputStream (m, "FileInputStream"); + py::class_, py::smart_holder> classFileInputStream (m, "FileInputStream"); classFileInputStream .def (py::init(), "fileToRead"_a) @@ -1821,7 +1821,7 @@ void registerYupCoreBindings (py::module_& m) .def ("failedToOpen", &FileInputStream::failedToOpen) .def ("openedOk", &FileInputStream::openedOk); - py::class_ classFileInputSource (m, "FileInputSource"); + py::class_ classFileInputSource (m, "FileInputSource"); classFileInputSource .def (py::init(), "file"_a, "useFileTimeInHashGeneration"_a = false); @@ -2105,7 +2105,7 @@ void registerYupCoreBindings (py::module_& m) // ============================================================================================ yup::URLInputSource - py::class_> classURLInputSource (m, "URLInputSource"); + py::class_, py::smart_holder> classURLInputSource (m, "URLInputSource"); classURLInputSource .def (py::init()); @@ -2484,7 +2484,7 @@ void registerYupCoreBindings (py::module_& m) // ============================================================================================ yup::XmlElement - py::class_> classXmlElement (m, "XmlElement"); + py::class_ classXmlElement (m, "XmlElement"); py::class_ classXmlElementTextFormat (classXmlElement, "TextFormat"); py::class_ classXmlElementComparator (classXmlElement, "Comparator"); @@ -2556,22 +2556,22 @@ void registerYupCoreBindings (py::module_& m) .def ("getChildElement", &XmlElement::getChildElement, py::return_value_policy::reference_internal) .def ("getChildByName", &XmlElement::getChildByName, py::return_value_policy::reference_internal) .def ("getChildByAttribute", &XmlElement::getChildByAttribute, py::return_value_policy::reference_internal) - .def ("addChildElement", [] (XmlElement& self, py::object newChildElement) + .def ("addChildElement", [] (XmlElement& self, std::unique_ptr newChildElement) { - self.addChildElement (newChildElement.release().cast()); + self.addChildElement (newChildElement.release()); }) - .def ("insertChildElement", [] (XmlElement& self, py::object newChildElement, int index) + .def ("insertChildElement", [] (XmlElement& self, std::unique_ptr newChildElement, int index) { - self.insertChildElement (newChildElement.release().cast(), index); + self.insertChildElement (newChildElement.release(), index); }) - .def ("prependChildElement", [] (XmlElement& self, py::object newChildElement) + .def ("prependChildElement", [] (XmlElement& self, std::unique_ptr newChildElement) { - self.prependChildElement (newChildElement.release().cast()); + self.prependChildElement (newChildElement.release()); }) .def ("createNewChildElement", &XmlElement::createNewChildElement, py::return_value_policy::reference_internal) - .def ("replaceChildElement", [] (XmlElement& self, XmlElement* currentChildElement, py::object newChildElement) + .def ("replaceChildElement", [] (XmlElement& self, XmlElement* currentChildElement, std::unique_ptr newChildElement) { - self.replaceChildElement (currentChildElement, newChildElement.release().cast()); + self.replaceChildElement (currentChildElement, newChildElement.release()); }) .def ("removeChildElement", &XmlElement::removeChildElement) .def ("deleteAllChildElements", &XmlElement::deleteAllChildElements) @@ -2614,9 +2614,9 @@ void registerYupCoreBindings (py::module_& m) .def ("getDocumentElement", &XmlDocument::getDocumentElement, "onlyReadOuterDocumentElement"_a = false) .def ("getDocumentElementIfTagMatches", &XmlDocument::getDocumentElementIfTagMatches, "requiredTag"_a) .def ("getLastParseError", &XmlDocument::getLastParseError) - .def ("setInputSource", [] (XmlDocument& self, py::object source) + .def ("setInputSource", [] (XmlDocument& self, std::unique_ptr source) { - self.setInputSource (source.release().cast()); + self.setInputSource (source.release()); }) .def ("setEmptyTextElementsIgnored", &XmlDocument::setEmptyTextElementsIgnored, "shouldBeIgnored"_a) .def_static ("parse", static_cast (*) (const File&)> (&XmlDocument::parse), "file"_a) @@ -2672,9 +2672,9 @@ void registerYupCoreBindings (py::module_& m) classZipFileBuilder .def (py::init<>()) .def ("addFile", &ZipFile::Builder::addFile, "fileToAdd"_a, "compressionLevel"_a, "storedPathName"_a = String()) - .def ("addEntry", [] (ZipFile::Builder& self, py::object stream, int compression, const String& path, Time time) + .def ("addEntry", [] (ZipFile::Builder& self, std::unique_ptr stream, int compression, const String& path, Time time) { - self.addEntry (stream.release().cast(), compression, path, time); + self.addEntry (stream.release(), compression, path, time); }, "streamToRead"_a, "compressionLevel"_a, "storedPathName"_a, "fileModificationTime"_a) .def ("writeToStream", [] (const ZipFile::Builder& self, OutputStream& target) { @@ -2684,9 +2684,9 @@ void registerYupCoreBindings (py::module_& m) classZipFile .def (py::init(), "file"_a) .def (py::init(), "inputStream"_a) - .def (py::init ([] (py::object inputSource) + .def (py::init ([] (std::unique_ptr inputSource) { - return new ZipFile (inputSource.release().cast()); + return new ZipFile (inputSource.release()); }), "inputSource"_a) .def ("getNumEntries", &ZipFile::getNumEntries) .def ("getIndexOfFileName", &ZipFile::getIndexOfFileName, "fileName"_a, "ignoreCase"_a = false) @@ -2798,6 +2798,475 @@ void registerYupCoreBindings (py::module_& m) registerSparseSet (m); + // ============================================================================================ yup::Logger + + py::class_ classLogger (m, "Logger"); + + classLogger + .def (py::init<>()) + .def_static ("setCurrentLogger", [] (py::object newLogger) + { + Logger::setCurrentLogger (newLogger.is_none() ? nullptr : &newLogger.cast()); + }, "newLogger"_a) + .def_static ("getCurrentLogger", []() -> py::object + { + if (auto* logger = Logger::getCurrentLogger()) + return py::cast (logger, py::return_value_policy::reference); + + return py::none(); + }) + .def_static ("writeToLog", &Logger::writeToLog, "message"_a) + .def_static ("outputDebugString", &Logger::outputDebugString, "text"_a); + + // ============================================================================================ yup::FileLogger + + py::class_ classFileLogger (m, "FileLogger"); + + classFileLogger + .def (py::init ([] (const File& fileToWriteTo, const String& welcomeMessage, int64 maxInitialFileSizeBytes) + { + return std::make_unique (fileToWriteTo, welcomeMessage, maxInitialFileSizeBytes); + }), "fileToWriteTo"_a, "welcomeMessage"_a, "maxInitialFileSizeBytes"_a = 128 * 1024) + .def ("getLogFile", &FileLogger::getLogFile, py::return_value_policy::reference_internal) + .def ("logMessage", &FileLogger::logMessage, "message"_a) + .def_static ("createDefaultAppLogger", [] (const String& logFileSubDirectoryName, const String& logFileName, const String& welcomeMessage, int64 maxInitialFileSizeBytes) + { + return std::unique_ptr (FileLogger::createDefaultAppLogger (logFileSubDirectoryName, logFileName, welcomeMessage, maxInitialFileSizeBytes)); + }, "logFileSubDirectoryName"_a, "logFileName"_a, "welcomeMessage"_a, "maxInitialFileSizeBytes"_a = 128 * 1024) + .def_static ("createDateStampedLogger", [] (const String& logFileSubDirectoryName, const String& logFileNameRoot, const String& logFileNameSuffix, const String& welcomeMessage) + { + return std::unique_ptr (FileLogger::createDateStampedLogger (logFileSubDirectoryName, logFileNameRoot, logFileNameSuffix, welcomeMessage)); + }, "logFileSubDirectoryName"_a, "logFileNameRoot"_a, "logFileNameSuffix"_a, "welcomeMessage"_a) + .def_static ("getSystemLogFileFolder", &FileLogger::getSystemLogFileFolder) + .def_static ("trimFileSize", &FileLogger::trimFileSize, "file"_a, "maxFileSize"_a); + + // ============================================================================================ yup::DynamicLibrary + + py::class_ classDynamicLibrary (m, "DynamicLibrary"); + + classDynamicLibrary + .def (py::init<>()) + .def (py::init(), "name"_a) + .def ("open", &DynamicLibrary::open, "name"_a) + .def ("close", &DynamicLibrary::close) + .def ("getFunction", [] (DynamicLibrary& self, const String& functionName) -> py::object + { + if (auto* function = self.getFunction (functionName)) + return py::capsule (function, "yup.dynamic_library_function"); + + return py::none(); + }, "functionName"_a) + .def ("getNativeHandle", [] (DynamicLibrary& self) -> py::object + { + if (auto* handle = self.getNativeHandle()) + return py::capsule (handle, "yup.dynamic_library_handle"); + + return py::none(); + }); + + // ============================================================================================ yup::SHA1 + + py::class_ classSHA1 (m, "SHA1"); + + classSHA1 + .def (py::init<>()) + .def (py::init(), "data"_a) + .def (py::init(), "file"_a) + .def (py::init ([] (py::buffer data) + { + const auto info = data.request(); + + return SHA1 (info.ptr, static_cast (info.size)); + }), "data"_a) + .def (py::init ([] (const String& text) + { + return SHA1 (text.toUTF8()); + }), "text"_a) + .def ("getRawData", [] (const SHA1& self) + { + const auto raw = self.getRawData(); + + return py::bytes (reinterpret_cast (raw.data()), raw.size()); + }) + .def ("toHexString", &SHA1::toHexString) + .def (py::self == py::self) + .def (py::self != py::self); + + // ============================================================================================ yup::CancelTokenSource + + py::class_ classCancelTokenSource (m, "CancelTokenSource"); + + classCancelTokenSource + .def (py::init<>()) + .def ("cancel", &CancelTokenSource::cancel) + .def ("getToken", &CancelTokenSource::getToken) + .def ("wasCancelled", &CancelTokenSource::wasCancelled) + .def ("isCancellable", &CancelTokenSource::isCancellable); + + // ============================================================================================ yup::CancelToken + + py::class_ classCancelToken (m, "CancelToken"); + + classCancelToken + .def (py::init<>()) + .def_static ("none", &CancelToken::none) + .def ("wasCancelled", &CancelToken::wasCancelled) + .def ("isCancellable", &CancelToken::isCancellable) + .def ("waitForCancellation", &CancelToken::waitForCancellation, "timeOutMilliseconds"_a = -1); + + // ============================================================================================ yup::WaitableTimer + + py::class_ classWaitableTimer (m, "WaitableTimer"); + + classWaitableTimer + .def (py::init<>()) + .def ("waitUntil", &WaitableTimer::waitUntil, "milliseconds"_a); + + // ============================================================================================ yup::StringPool + + py::class_ classStringPool (m, "StringPool"); + + classStringPool + .def (py::init<>()) + .def ("getPooledString", py::overload_cast (&StringPool::getPooledString), "original"_a) + .def ("getPooledString", py::overload_cast (&StringPool::getPooledString), "original"_a) + .def ("garbageCollect", &StringPool::garbageCollect) + .def_static ("getGlobalPool", &StringPool::getGlobalPool, py::return_value_policy::reference); + + // ============================================================================================ yup::TextDiff + + py::class_ classTextDiff (m, "TextDiff"); + py::class_ classTextDiffChange (classTextDiff, "Change"); + + classTextDiffChange + .def_readonly ("insertedText", &TextDiff::Change::insertedText) + .def_readonly ("start", &TextDiff::Change::start) + .def_readonly ("length", &TextDiff::Change::length) + .def ("isDeletion", &TextDiff::Change::isDeletion) + .def ("appliedTo", &TextDiff::Change::appliedTo, "original"_a); + + classTextDiff + .def (py::init(), "original"_a, "target"_a) + .def ("appliedTo", &TextDiff::appliedTo, "text"_a) + .def ("getChanges", [] (const TextDiff& self) + { + py::list result; + + for (const auto& change : self.changes) + result.append (change); + + return result; + }); + + // ============================================================================================ yup::DynamicObject + + py::class_> classDynamicObject (m, "DynamicObject"); + + classDynamicObject + .def (py::init<>()) + .def ("hasProperty", &DynamicObject::hasProperty, "propertyName"_a) + .def ("getProperty", py::overload_cast (&DynamicObject::getProperty, py::const_), "propertyName"_a) + .def ("getProperty", py::overload_cast (&DynamicObject::getProperty, py::const_), "propertyName"_a, "defaultValue"_a) + .def ("setProperty", &DynamicObject::setProperty, "propertyName"_a, "newValue"_a) + .def ("removeProperty", &DynamicObject::removeProperty, "propertyName"_a) + .def ("hasMethod", &DynamicObject::hasMethod, "methodName"_a) + .def ("clear", &DynamicObject::clear) + .def ("cloneAllProperties", &DynamicObject::cloneAllProperties) + .def ("getProperties", [] (DynamicObject& self) -> NamedValueSet& + { + return self.getProperties(); + }, py::return_value_policy::reference_internal); + + // ============================================================================================ yup::AbstractFifo + + py::class_ classAbstractFifo (m, "AbstractFifo"); + + classAbstractFifo + .def (py::init(), "capacity"_a) + .def ("getTotalSize", &AbstractFifo::getTotalSize) + .def ("getFreeSpace", &AbstractFifo::getFreeSpace) + .def ("getNumReady", &AbstractFifo::getNumReady) + .def ("reset", &AbstractFifo::reset) + .def ("setTotalSize", &AbstractFifo::setTotalSize, "newSize"_a) + .def ("prepareToWrite", [] (const AbstractFifo& self, int numToWrite) + { + int startIndex1 = 0, blockSize1 = 0, startIndex2 = 0, blockSize2 = 0; + + self.prepareToWrite (numToWrite, startIndex1, blockSize1, startIndex2, blockSize2); + + return py::make_tuple (startIndex1, blockSize1, startIndex2, blockSize2); + }, "numToWrite"_a) + .def ("finishedWrite", &AbstractFifo::finishedWrite, "numWritten"_a) + .def ("prepareToRead", [] (const AbstractFifo& self, int numWanted) + { + int startIndex1 = 0, blockSize1 = 0, startIndex2 = 0, blockSize2 = 0; + + self.prepareToRead (numWanted, startIndex1, blockSize1, startIndex2, blockSize2); + + return py::make_tuple (startIndex1, blockSize1, startIndex2, blockSize2); + }, "numWanted"_a) + .def ("finishedRead", &AbstractFifo::finishedRead, "numRead"_a); + + // ============================================================================================ yup::SingleThreadedAbstractFifo + + py::class_ classSingleThreadedAbstractFifo (m, "SingleThreadedAbstractFifo"); + + classSingleThreadedAbstractFifo + .def (py::init<>()) + .def (py::init(), "size"_a) + .def ("getRemainingSpace", &SingleThreadedAbstractFifo::getRemainingSpace) + .def ("getNumReadable", &SingleThreadedAbstractFifo::getNumReadable) + .def ("getSize", &SingleThreadedAbstractFifo::getSize) + .def ("write", [] (SingleThreadedAbstractFifo& self, int num) + { + return self.write (num); + }, "num"_a) + .def ("read", [] (SingleThreadedAbstractFifo& self, int num) + { + return self.read (num); + }, "num"_a); + + // ============================================================================================ yup::StatisticsAccumulator<> + + registerStatisticsAccumulator (m); + + // ============================================================================================ yup::Expression + + py::class_ classExpression (m, "Expression"); + py::class_ classExpressionScope (classExpression, "Scope"); + + classExpressionScope + .def (py::init<>()) + .def ("getScopeUID", &Expression::Scope::getScopeUID) + .def ("getSymbolValue", &Expression::Scope::getSymbolValue, "symbol"_a); + + classExpression + .def (py::init<>()) + .def (py::init(), "constant"_a) + .def (py::init ([] (const String& stringToParse) + { + String parseError; + auto result = Expression (stringToParse, parseError); + + if (parseError.isNotEmpty()) + throw py::value_error (parseError.toStdString()); + + return result; + }), "stringToParse"_a) + .def ("toString", &Expression::toString) + .def (py::self + py::self) + .def (py::self - py::self) + .def (py::self * py::self) + .def (py::self / py::self) + .def (- py::self) + .def_static ("symbol", &Expression::symbol, "symbol"_a) + .def_static ("function", [] (const String& functionName, py::list parameters) + { + Array parameterList; + + for (auto item : parameters) + parameterList.add (item.cast()); + + return Expression::function (functionName, parameterList); + }, "functionName"_a, "parameters"_a) + .def ("evaluate", py::overload_cast<> (&Expression::evaluate, py::const_)) + .def ("evaluate", py::overload_cast (&Expression::evaluate, py::const_), "scope"_a) + .def ("adjustedToGiveNewResult", &Expression::adjustedToGiveNewResult, "targetValue"_a, "scope"_a) + .def ("usesAnySymbols", &Expression::usesAnySymbols); + + // ============================================================================================ yup::LocalisedStrings + + py::class_ classLocalisedStrings (m, "LocalisedStrings"); + + classLocalisedStrings + .def (py::init ([] (const String& fileContents, bool ignoreCaseOfKeys) + { + return std::make_unique (fileContents, ignoreCaseOfKeys); + }), "fileContents"_a, "ignoreCaseOfKeys"_a) + .def (py::init ([] (const File& fileToLoad, bool ignoreCaseOfKeys) + { + return std::make_unique (fileToLoad, ignoreCaseOfKeys); + }), "fileToLoad"_a, "ignoreCaseOfKeys"_a) + .def ("translate", py::overload_cast (&LocalisedStrings::translate, py::const_), "text"_a) + .def ("translate", py::overload_cast (&LocalisedStrings::translate, py::const_), "text"_a, "resultIfNotFound"_a) + .def ("getLanguageName", &LocalisedStrings::getLanguageName) + .def ("getCountryCodes", &LocalisedStrings::getCountryCodes, py::return_value_policy::reference_internal) + .def ("getMappings", &LocalisedStrings::getMappings, py::return_value_policy::reference_internal) + .def ("addStrings", &LocalisedStrings::addStrings, "other"_a) + .def_static ("translateWithCurrentMappings", py::overload_cast (&LocalisedStrings::translateWithCurrentMappings), "text"_a) + .def_static ("translateWithCurrentMappings", py::overload_cast (&LocalisedStrings::translateWithCurrentMappings), "text"_a); + + // ============================================================================================ yup::YAML + + py::class_ classYAML (m, "YAML"); + py::class_ classYAMLFormatOptions (classYAML, "FormatOptions"); + + Helpers::makeArithmeticEnum (classYAML, "Spacing") + .value ("none", YAML::Spacing::none) + .value ("singleLine", YAML::Spacing::singleLine) + .value ("multiLine", YAML::Spacing::multiLine) + .export_values(); + + classYAMLFormatOptions + .def (py::init<>()) + .def ("withSpacing", &YAML::FormatOptions::withSpacing, "spacing"_a) + .def ("withMaxDecimalPlaces", &YAML::FormatOptions::withMaxDecimalPlaces, "maxDecimalPlaces"_a) + .def ("withIndentLevel", &YAML::FormatOptions::withIndentLevel, "indentLevel"_a) + .def ("getSpacing", &YAML::FormatOptions::getSpacing) + .def ("getMaxDecimalPlaces", &YAML::FormatOptions::getMaxDecimalPlaces) + .def ("getIndentLevel", &YAML::FormatOptions::getIndentLevel); + + classYAML + .def_static ("parse", py::overload_cast (&YAML::parse), "text"_a) + .def_static ("parse", py::overload_cast (&YAML::parse), "file"_a) + .def_static ("parseWithResult", [] (const String& text) + { + var parsedResult; + auto result = YAML::parse (text, parsedResult); + + return py::make_tuple (result, parsedResult); + }, "text"_a) + .def_static ("fromString", &YAML::fromString, "text"_a) + .def_static ("toString", py::overload_cast (&YAML::toString), "objectToFormat"_a, "allOnOneLine"_a = false, "maximumDecimalPlaces"_a = 15) + .def_static ("toString", py::overload_cast (&YAML::toString), "objectToFormat"_a, "formatOptions"_a) + .def_static ("escapeString", &YAML::escapeString, "text"_a); + + // ============================================================================================ yup::IPAddress + + py::class_ classIPAddress (m, "IPAddress"); + + classIPAddress + .def (py::init<>()) + .def (py::init(), "address1"_a, "address2"_a, "address3"_a, "address4"_a) + .def (py::init ([] (uint32 asNativeEndian32Bit) + { + return IPAddress (asNativeEndian32Bit); + }), "asNativeEndian32Bit"_a) + .def (py::init(), "address"_a) + .def_readonly ("isIPv6", &IPAddress::isIPv6) + .def ("isNull", &IPAddress::isNull) + .def ("toString", &IPAddress::toString) + .def ("compare", &IPAddress::compare, "other"_a) + .def ("getAddressBytes", [] (const IPAddress& self) + { + const auto numBytes = self.isIPv6 ? 16 : 4; + + return py::bytes (reinterpret_cast (self.address), static_cast (numBytes)); + }) + .def (py::self == py::self) + .def (py::self != py::self) + .def (py::self < py::self) + .def (py::self > py::self) + .def (py::self <= py::self) + .def (py::self >= py::self) + .def_static ("any", &IPAddress::any) + .def_static ("broadcast", &IPAddress::broadcast) + .def_static ("local", &IPAddress::local, "IPv6"_a = false) + .def_static ("getLocalAddress", &IPAddress::getLocalAddress, "includeIPv6"_a = false) + .def_static ("getAllAddresses", [] (bool includeIPv6) + { + py::list result; + + for (const auto& address : IPAddress::getAllAddresses (includeIPv6)) + result.append (address); + + return result; + }, "includeIPv6"_a = false) + .def_static ("getFormattedAddress", &IPAddress::getFormattedAddress, "unformattedAddress"_a) + .def_static ("isIPv4MappedAddress", &IPAddress::isIPv4MappedAddress, "mappedAddress"_a) + .def_static ("convertIPv4MappedAddressToIPv4", &IPAddress::convertIPv4MappedAddressToIPv4, "mappedAddress"_a) + .def_static ("convertIPv4AddressToIPv4Mapped", &IPAddress::convertIPv4AddressToIPv4Mapped, "addressToMap"_a) + .def_static ("getInterfaceBroadcastAddress", &IPAddress::getInterfaceBroadcastAddress, "interfaceAddress"_a); + + // ============================================================================================ yup::MACAddress + + py::class_ classMACAddress (m, "MACAddress"); + + classMACAddress + .def (py::init<>()) + .def (py::init ([] (py::buffer bytes) + { + const auto info = bytes.request(); + + if (info.size < 6) + throw py::value_error ("A MACAddress needs at least 6 bytes"); + + return MACAddress (static_cast (info.ptr)); + }), "bytes"_a) + .def (py::init(), "address"_a) + .def ("toString", py::overload_cast<> (&MACAddress::toString, py::const_)) + .def ("toString", py::overload_cast (&MACAddress::toString, py::const_), "separator"_a) + .def ("toInt64", &MACAddress::toInt64) + .def ("isNull", &MACAddress::isNull) + .def ("getBytes", [] (const MACAddress& self) + { + return py::bytes (reinterpret_cast (self.getBytes()), 6); + }) + .def (py::self == py::self) + .def (py::self != py::self) + .def_static ("getAllAddresses", []() + { + py::list result; + + for (const auto& address : MACAddress::getAllAddresses()) + result.append (address); + + return result; + }); + + // ============================================================================================ yup::NamedPipe + + py::class_ classNamedPipe (m, "NamedPipe"); + + classNamedPipe + .def (py::init<>()) + .def ("openExisting", &NamedPipe::openExisting, "pipeName"_a) + .def ("createNewPipe", &NamedPipe::createNewPipe, "pipeName"_a, "mustNotExist"_a = false) + .def ("close", &NamedPipe::close) + .def ("isOpen", &NamedPipe::isOpen) + .def ("getName", &NamedPipe::getName) + .def ("read", [] (NamedPipe& self, py::buffer destBuffer, int timeOutMilliseconds) + { + auto info = destBuffer.request (true); + + return self.read (info.ptr, static_cast (info.size), timeOutMilliseconds); + }, "destBuffer"_a, "timeOutMilliseconds"_a) + .def ("write", [] (NamedPipe& self, py::buffer sourceBuffer, int timeOutMilliseconds) + { + const auto info = sourceBuffer.request(); + + return self.write (info.ptr, static_cast (info.size), timeOutMilliseconds); + }, "sourceBuffer"_a, "timeOutMilliseconds"_a); + + // ============================================================================================ yup::WebInputStream + + py::class_, py::smart_holder> classWebInputStream (m, "WebInputStream"); + py::class_ classWebInputStreamListener (classWebInputStream, "Listener"); + + classWebInputStreamListener + .def (py::init<>()) + .def ("postDataSendProgress", &WebInputStream::Listener::postDataSendProgress, "request"_a, "bytesSent"_a, "totalBytes"_a); + + classWebInputStream + .def (py::init(), "url"_a, "addParametersToRequestBody"_a) + .def ("withExtraHeaders", &WebInputStream::withExtraHeaders, "extraHeaders"_a, py::return_value_policy::reference_internal) + .def ("withCustomRequestCommand", &WebInputStream::withCustomRequestCommand, "customRequestCommand"_a, py::return_value_policy::reference_internal) + .def ("withConnectionTimeout", &WebInputStream::withConnectionTimeout, "timeoutInMs"_a, py::return_value_policy::reference_internal) + .def ("withNumRedirectsToFollow", &WebInputStream::withNumRedirectsToFollow, "numRedirects"_a, py::return_value_policy::reference_internal) + .def ("connect", [] (WebInputStream& self) + { + return self.connect (nullptr); + }) + .def ("connect", [] (WebInputStream& self, WebInputStream::Listener& listener) + { + return self.connect (&listener); + }, "listener"_a) + .def ("isError", &WebInputStream::isError) + .def ("cancel", &WebInputStream::cancel) + .def ("getRequestHeaders", &WebInputStream::getRequestHeaders) + .def ("getResponseHeaders", &WebInputStream::getResponseHeaders) + .def ("getStatusCode", &WebInputStream::getStatusCode); + // ============================================================================================ testing m.def ("__raise_cpp_exception__", [] (const yup::String& what) diff --git a/modules/yup_python/bindings/yup_YupCore_bindings.h b/modules/yup_python/bindings/yup_YupCore_bindings.h index 53558b6a3..b49c8c192 100644 --- a/modules/yup_python/bindings/yup_YupCore_bindings.h +++ b/modules/yup_python/bindings/yup_YupCore_bindings.h @@ -369,6 +369,47 @@ void registerArray (pybind11::module_& m) //============================================================================== +template