diff --git a/README.md b/README.md index 44a8265..b6d0bf1 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,11 @@ # About -A modern library targetting C++20 and SDL3 for cross-platform, immediate-mode, desktop application development. With *laya*, you can create windows, handle input events, render 2D graphics and manage resources in a type-safe and efficient manner while leveraging the full power of the underlying SDL library. +A modern library targetting C++20 and SDL3 for cross-platform, immediate-mode, desktop application development. With *laya*, you can create windows, handle input events, render 2D graphics, upload textures from surfaces, and manage resources in a type-safe and efficient manner while leveraging the full power of the underlying SDL library. + +> **PNG note**: `surface::load_png`/`save_png` and `texture::load_png` require SDL_image integration, which is not yet wired up in this branch. Use BMP helpers or bring your own PNG loader for now. See [Surface docs](docs/features/surfaces.md#file-io) and [Texture docs](docs/features/textures.md#limitations--future-work) for the latest status. + +> **Locking note**: Surfaces generally do not require explicit locking in SDL3, but `surface_lock_guard` is available for compatibility. Textures support regional locking through `texture_lock_guard`. The [Rendering docs](docs/features/rendering.md#textures-and-surfaces) outline integration tips and gotchas.
example diff --git a/docs/features/rendering.md b/docs/features/rendering.md index 6510c0f..cd80342 100644 --- a/docs/features/rendering.md +++ b/docs/features/rendering.md @@ -101,6 +101,34 @@ while (running) { --- +## Textures and Surfaces + +You can upload pixels from an SDL surface, tint them, and draw them like any other primitive. This is useful when loading BMP files (built-in) or PNG files (via SDL_image). + +```cpp +laya::context ctx{laya::subsystem::video}; +laya::window win{"Textures", {800, 600}}; +laya::renderer ren{win}; + +// Load a surface from disk. +auto surf = laya::surface::load_bmp("assets/logo.bmp"); +// Create a texture bound to the renderer. +auto tex = laya::texture::from_surface(ren, surf); +tex.set_color_mod(laya::color{255, 255, 255}); + +ren.clear(laya::color::black()); +ren.render(tex, {100, 100, 256, 256}); +ren.present(); +``` + +See the dedicated [Surfaces](surfaces.md) and [Textures](textures.md) feature pages for a deeper dive. + +### Limitations + +- `surface::load_png` and `surface::save_png` currently throw until SDL_image support is integrated. +- `texture::load_png` mirrors this limitation because it depends on surfaces. +- Regional texture locking is supported, but surfaces generally do not require locking in SDL3; `surface::must_lock()` returns `false` for now. + ## Native Handle Access the underlying SDL renderer for interop: diff --git a/docs/features/surfaces.md b/docs/features/surfaces.md new file mode 100644 index 0000000..8afb7b0 --- /dev/null +++ b/docs/features/surfaces.md @@ -0,0 +1,103 @@ +# Surfaces + +CPU-resident pixel buffers for loading images, running software draw routines, and staging texture uploads. + +## Creating Surfaces + +```cpp +#include + +laya::surface from_args{laya::surface_args{ + .size = {256, 256}, + .format = laya::pixel_format::rgba32, + .flags = laya::surface_flags::rle_optimized, +}}; + +laya::surface from_size{{128, 128}}; // Defaults to RGBA32 +laya::surface from_bmp = laya::surface::load_bmp("ui/logo.bmp"); +``` + +> **PNG support** — `surface::load_png`/`save_png` currently throw until SDL_image is wired up. Use BMP helpers or provide your own loader if you need PNG today. + +## Filling and Blitting + +```cpp +from_args.fill(laya::color::black()); +from_args.fill_rect({32, 32, 96, 96}, laya::color{255, 0, 0}); + +std::array bars{ + laya::rect{0, 0, 64, 256}, + laya::rect{192, 0, 64, 256}, +}; +from_args.fill_rects(bars, laya::color::white()); + +from_args.blit(from_bmp, {0, 0, 64, 64}, {160, 160, 64, 64}); +from_args.blit(from_bmp, {50, 50}); +``` + +## Transformations + +Transformations return new `laya::surface` instances, preserving RAII semantics: + +```cpp +auto copy = from_bmp.duplicate(); +auto converted = from_bmp.convert(laya::pixel_format::bgra32); +auto scaled = from_bmp.scale({512, 512}); +auto flipped = from_bmp.flip(laya::flip_mode::horizontal); +``` + +Scaling currently uses linear filtering; configurable scale modes will arrive with future renderer updates. + +## State Management + +```cpp +copy.set_alpha_mod(192); +copy.set_color_mod({200, 255, 200}); +copy.set_blend_mode(laya::blend_mode::blend); + +if (copy.has_color_key()) { + copy.clear_color_key(); +} +``` + +Color key values respect the surface pixel format internally by querying SDL’s format metadata. + +## Locking Pixels + +Use `surface_lock_guard` for direct pixel access. The guard captures the raw pointer/pitch up front and automatically unlocks when destroyed. + +```cpp +{ + auto lock = copy.lock(); + std::uint8_t* pixels = static_cast(lock.pixels()); + const int pitch = lock.pitch(); + // mutate pixels here +} // unlocked automatically +``` + +SDL3 rarely requires locking for software surfaces; `surface::must_lock()` returns `false` until SDL exposes richer metadata, but the guard keeps the API consistent. + +## File IO + +```cpp +from_args.save_bmp("out/debug.bmp"); +// from_args.save_png("out/debug.png"); // throws until SDL_image support +``` + +## Integrating with Textures + +Surfaces are ideal staging buffers for GPU textures: + +```cpp +laya::renderer renderer{window}; +auto sprite_surface = laya::surface::load_bmp("assets/sprite.bmp"); +auto sprite_texture = laya::texture::from_surface(renderer, sprite_surface); +``` + +See [Textures](textures.md) for the GPU side of the pipeline. + +## Limitations & Future Work + +- PNG helpers require SDL_image and will throw until that dependency is integrated. +- Scale mode is fixed to linear filtering; configurable scale modes will be added later. +- `surface_args::flags` currently support `rle_optimized`; additional SDL surface flags can be surfaced if needed. diff --git a/docs/features/textures.md b/docs/features/textures.md new file mode 100644 index 0000000..b93a91a --- /dev/null +++ b/docs/features/textures.md @@ -0,0 +1,100 @@ +# Textures + +GPU resources backed by SDL_Renderer for fast blitting, tinting, and scaling. + +## Creating Textures + +```cpp +#include + +laya::context ctx{laya::subsystem::video}; +laya::window window{"Textures", {800, 600}}; +laya::renderer renderer{window}; + +laya::texture tex_from_args{renderer, laya::texture_args{ + .format = laya::pixel_format::rgba32, + .size = {256, 256}, + .access = laya::texture_access::streaming, +}}; + +auto from_surface = laya::texture::from_surface(renderer, laya::surface::load_bmp("sprite.bmp")); +``` + +> **PNG support** — `texture::load_png` depends on `surface::load_png`, so it currently throws until SDL_image wiring lands. + +## Updating Pixels + +Upload CPU buffers directly: + +```cpp +std::vector pixels(256 * 256, 0xFF00FF00); +tex_from_args.update(pixels.data(), 256 * sizeof(std::uint32_t)); + +laya::rect region{32, 32, 64, 64}; +tex_from_args.update(region, pixels.data(), 256 * sizeof(std::uint32_t)); +``` + +Or lock for streaming writes: + +```cpp +{ + auto lock = tex_from_args.lock(); + auto* row = static_cast(lock.pixels()); + for (int y = 0; y < tex_from_args.size().height; ++y) { + std::fill_n(row, tex_from_args.size().width * 4, 0x7F); + row += lock.pitch(); + } +} +``` + +Regional locking leverages SDL3’s built-in support via `texture::lock(const rect&)`. + +## Rendering + +Renderer helpers cover common blit/transform combos: + +```cpp +laya::renderer ren{window}; +ren.clear(); +ren.render(from_surface, {100, 100}); // position +ren.render(from_surface, {200, 200, 64, 64}); // destination rect +ren.render(from_surface, {0, 0, 32, 32}, {320, 200, 64, 64}); // src/dst +ren.render(from_surface, {320, 320, 64, 64}, 45.0); // rotation +ren.render(from_surface, {320, 320, 64, 64}, 0.0, {32, 32}); // custom pivot +ren.render(from_surface, {0, 0, 128, 128}, {400, 100, 128, 128}, + 0.0, {0, 0}, laya::flip_mode::horizontal); +ren.present(); +``` + +All rectangle coordinates convert to `SDL_FRect` internally, so integer inputs stay precise while enabling subpixel rendering. + +## Modulation & State + +```cpp +from_surface.set_alpha_mod(200); +from_surface.set_color_mod({255, 200, 200}); +from_surface.set_blend_mode(laya::blend_mode::blend); +from_surface.set_scale_mode(laya::scale_mode::linear); + +auto alpha = from_surface.get_alpha_mod(); +auto color = from_surface.get_color_mod(); +``` + +Scale modes map directly to SDL scale filters; `nearest` and `linear` are available today. `SDL_SCALEMODE_BEST` is unavailable in SDL3 and purposely omitted. + +## Metadata + +Texture instances cache their size/format/access metadata when created, so queries avoid expensive `SDL_GetTextureProperties` calls at runtime: + +```cpp +auto size = from_surface.size(); // returns cached dimentions +auto format = from_surface.format(); +auto access = from_surface.access(); +``` + +## Limitations & Future Work + +- PNG helpers throw until SDL_image integration is complete. +- `texture::from_surface` currently duplicates metadata queries; future updates will streamline this when SDL adds richer creation APIs. +- Renderer helpers currently accept `laya::rect`/`laya::point`; span-based batching is planned for future revisions. +- `texture::load_*` helpers are synchronous; async/background loading is left to higher-level systems. diff --git a/include/laya/laya.hpp b/include/laya/laya.hpp index 4c3321a..fc6a307 100644 --- a/include/laya/laya.hpp +++ b/include/laya/laya.hpp @@ -7,6 +7,11 @@ #include "events/event_types.hpp" #include "events/event_polling.hpp" #include "renderers/renderer.hpp" +#include "surfaces/pixel_format.hpp" +#include "surfaces/surface_flags.hpp" +#include "surfaces/surface.hpp" +#include "textures/texture_access.hpp" +#include "textures/texture.hpp" #include "windows/window.hpp" #include "subsystems.hpp" #include "errors.hpp" diff --git a/include/laya/renderers/renderer.hpp b/include/laya/renderers/renderer.hpp index a23dae6..ffdd7eb 100644 --- a/include/laya/renderers/renderer.hpp +++ b/include/laya/renderers/renderer.hpp @@ -7,6 +7,7 @@ #include "renderer_flags.hpp" #include "renderer_id.hpp" #include "renderer_types.hpp" +#include struct SDL_Renderer; @@ -14,6 +15,7 @@ namespace laya { // Forward declarations class window; +class texture; // ============================================================================ // Renderer creation arguments @@ -193,6 +195,35 @@ class renderer { /// Fill multiple rectangles with the current draw color void fill_rects(const rect* rects, int count); + // ======================================================================== + // Texture rendering operations + // ======================================================================== + + /// Render entire texture at destination position + void render(const texture& tex, point dst_pos); + + /// Render entire texture to destination rectangle + void render(const texture& tex, const rect& dst_rect); + + /// Render texture region to destination rectangle + void render(const texture& tex, const rect& src_rect, const rect& dst_rect); + + /// Render texture with rotation around center + void render(const texture& tex, const rect& dst_rect, double angle); + + /// Render texture with rotation around specified center point + void render(const texture& tex, const rect& dst_rect, double angle, point center); + + /// Render texture with full control (rotation, center, flip) + void render(const texture& tex, const rect& src_rect, const rect& dst_rect, double angle, point center, + flip_mode flip); + + /// Render texture with flipping + void render(const texture& tex, const rect& dst_rect, flip_mode flip); + + /// Render part of texture with flipping + void render(const texture& tex, const rect& src_rect, const rect& dst_rect, flip_mode flip); + // ======================================================================== // Accessors // ======================================================================== diff --git a/include/laya/renderers/renderer_types.hpp b/include/laya/renderers/renderer_types.hpp index e1e4f74..4c1136e 100644 --- a/include/laya/renderers/renderer_types.hpp +++ b/include/laya/renderers/renderer_types.hpp @@ -165,6 +165,18 @@ enum class blend_mode : std::uint32_t { mul = 0x00000008 ///< Color multiplication }; +// ============================================================================ +// Flip modes +// ============================================================================ + +/// Flip modes for surface and texture transformations +/// Maps directly to SDL_FlipMode values for type safety +enum class flip_mode : int { + none = 0, ///< No flipping (SDL_FLIP_NONE) + horizontal = 1, ///< Horizontal flip (SDL_FLIP_HORIZONTAL) + vertical = 2 ///< Vertical flip (SDL_FLIP_VERTICAL) +}; + // ============================================================================ // VSync modes // ============================================================================ diff --git a/include/laya/surfaces/pixel_format.hpp b/include/laya/surfaces/pixel_format.hpp new file mode 100644 index 0000000..cb54863 --- /dev/null +++ b/include/laya/surfaces/pixel_format.hpp @@ -0,0 +1,22 @@ +/// @file pixel_format.hpp +/// @brief Pixel format enumeration for surface operations +/// @date 2025-12-10 + +#pragma once + +#include + +namespace laya { + +/// Pixel format enumeration for surfaces and textures +enum class pixel_format : std::uint32_t { + unknown = 0, + rgba32 = 0x16462004, ///< SDL_PIXELFORMAT_RGBA32 - 32-bit RGBA format + argb32 = 0x16362004, ///< SDL_PIXELFORMAT_ARGB32 - 32-bit ARGB format + bgra32 = 0x16762004, ///< SDL_PIXELFORMAT_BGRA32 - 32-bit BGRA format + abgr32 = 0x16662004, ///< SDL_PIXELFORMAT_ABGR32 - 32-bit ABGR format + rgb24 = 0x17101803, ///< SDL_PIXELFORMAT_RGB24 - 24-bit RGB format + bgr24 = 0x17401803 ///< SDL_PIXELFORMAT_BGR24 - 24-bit BGR format +}; + +} // namespace laya diff --git a/include/laya/surfaces/surface.hpp b/include/laya/surfaces/surface.hpp new file mode 100644 index 0000000..adc0e72 --- /dev/null +++ b/include/laya/surfaces/surface.hpp @@ -0,0 +1,117 @@ +/// @file surface.hpp +/// @brief Surface RAII wrapper for SDL3 surface operations +/// @date 2025-12-10 + +#pragma once + +#include +#include +#include + +#include "../renderers/renderer_types.hpp" +#include "../windows/window_flags.hpp" +#include "pixel_format.hpp" +#include "surface_flags.hpp" + +// Forward declarations +struct SDL_Surface; + +namespace laya { + +/// Arguments for surface creation +struct surface_args { + dimentions size; ///< Surface dimensions + pixel_format format = pixel_format::rgba32; ///< Pixel format + surface_flags flags = surface_flags::none; ///< Creation flags (currently only rle_optimized is applied) +}; + +/// RAII lock guard for surface pixel access +class surface_lock_guard { +public: + explicit surface_lock_guard(class surface& surf); + ~surface_lock_guard() noexcept; + + // Non-copyable but movable + surface_lock_guard(const surface_lock_guard&) = delete; + surface_lock_guard& operator=(const surface_lock_guard&) = delete; + surface_lock_guard(surface_lock_guard&& other) noexcept; + surface_lock_guard& operator=(surface_lock_guard&& other) noexcept; + + /// Get direct pixel data pointer + [[nodiscard]] void* pixels() const noexcept; + + /// Get pitch (row stride in bytes) + [[nodiscard]] int pitch() const noexcept; + +private: + class surface* m_surface; + void* m_pixels{nullptr}; + int m_pitch{0}; +}; + +/// RAII wrapper for SDL_Surface +class surface { +public: + // Creation + explicit surface(const surface_args& args); + surface(dimentions size, pixel_format format = pixel_format::rgba32); + + /// Load surface from BMP file + [[nodiscard]] static surface load_bmp(std::string_view path); + + /// Load surface from PNG file (requires SDL_image) + [[nodiscard]] static surface load_png(std::string_view path); + + // RAII + ~surface() noexcept; + surface(const surface&) = delete; + surface& operator=(const surface&) = delete; + surface(surface&& other) noexcept; + surface& operator=(surface&& other) noexcept; + + // Operations (throw laya::error on failure) + void fill(color c); + void fill_rect(const rect& r, color c); + void fill_rects(std::span rects, color c); + void clear(); + void blit(const surface& src, const rect& src_rect, const rect& dst_rect); + void blit(const surface& src, point dst_pos); + + // Transformations (return new surface) + [[nodiscard]] surface convert(pixel_format format) const; + [[nodiscard]] surface duplicate() const; + [[nodiscard]] surface scale(dimentions new_size) const; + [[nodiscard]] surface flip(flip_mode mode) const; + + // State management + void set_alpha_mod(std::uint8_t alpha); + void set_color_mod(color c); + void set_color_mod(std::uint8_t r, std::uint8_t g, std::uint8_t b); + void set_blend_mode(blend_mode mode); + void set_color_key(color key); + void clear_color_key(); + + // Getters (noexcept) + [[nodiscard]] std::uint8_t get_alpha_mod() const; + [[nodiscard]] color get_color_mod() const; + [[nodiscard]] blend_mode get_blend_mode() const; + [[nodiscard]] bool has_color_key() const; + [[nodiscard]] color get_color_key() const; + [[nodiscard]] dimentions size() const noexcept; + [[nodiscard]] pixel_format format() const noexcept; + [[nodiscard]] bool must_lock() const noexcept; + + // Low-level access + [[nodiscard]] surface_lock_guard lock(); + void save_bmp(std::string_view path) const; + void save_png(std::string_view path) const; + + /// Get native SDL surface handle + [[nodiscard]] SDL_Surface* native_handle() const noexcept; + +private: + SDL_Surface* m_surface; + explicit surface(SDL_Surface* surf); // For factory methods +}; + +} // namespace laya diff --git a/include/laya/surfaces/surface_flags.hpp b/include/laya/surfaces/surface_flags.hpp new file mode 100644 index 0000000..d0255bc --- /dev/null +++ b/include/laya/surfaces/surface_flags.hpp @@ -0,0 +1,22 @@ +/// @file surface_flags.hpp +/// @brief Surface creation flags for surface operations +/// @date 2025-12-10 + +#pragma once + +#include "../bitmask.hpp" + +namespace laya { + +/// Surface creation and operation flags +enum class surface_flags : unsigned { + none = 0, + preallocated = 0x00000001, ///< Surface uses preallocated memory + rle_optimized = 0x00000002 ///< Surface is RLE optimized for fast blitting +}; + +/// Enable bitmask operations for surface_flags +template <> +struct enable_bitmask_operators : std::true_type {}; + +} // namespace laya diff --git a/include/laya/textures/texture.hpp b/include/laya/textures/texture.hpp new file mode 100644 index 0000000..559958f --- /dev/null +++ b/include/laya/textures/texture.hpp @@ -0,0 +1,255 @@ +/// RAII wrapper for SDL3 textures with comprehensive texture operations. +/// \file texture.hpp +/// \date 2025-09-28 + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +struct SDL_Texture; + +namespace laya { + +// Forward declarations +class renderer; + +/// Arguments for texture construction. +struct texture_args { + /// Pixel format for the texture. + pixel_format format = pixel_format::rgba32; + + /// Texture dimensions. + dimentions size{0, 0}; + + /// Access pattern for the texture. + texture_access access = texture_access::static_; +}; + +/// RAII guard for texture pixel access during lock operations. +/// Automatically unlocks the texture on destruction. +class texture_lock_guard { +public: + /// Locks the texture for pixel access. + /// \param tex Texture to lock. + /// \param region Optional region to lock. + /// \throws laya::error if the texture cannot be locked. + explicit texture_lock_guard(class texture& tex, const rect* region = nullptr); + + /// Unlocks the texture automatically. + ~texture_lock_guard() noexcept; + + // Non-copyable but movable + texture_lock_guard(const texture_lock_guard&) = delete; + texture_lock_guard& operator=(const texture_lock_guard&) = delete; + texture_lock_guard(texture_lock_guard&& other) noexcept; + texture_lock_guard& operator=(texture_lock_guard&& other) noexcept; + + /// Gets raw pixel data pointer. + /// \returns Non-owning pointer to pixel data. + [[nodiscard]] void* pixels() const noexcept; + + /// Gets texture pitch (bytes per row). + /// \returns Number of bytes per row. + [[nodiscard]] int pitch() const noexcept; + +private: + class texture* m_texture{nullptr}; + void* m_pixels{nullptr}; + int m_pitch{0}; +}; + +/// RAII wrapper for SDL3 textures. +/// Move-only semantics ensure proper resource management. +class texture { +public: + // ======================================================================== + // Construction and destruction + // ======================================================================== + + /// Creates a texture with the specified parameters. + /// \param renderer Renderer to create the texture for. + /// \param args Texture creation arguments. + /// \throws laya::error if texture creation fails. + texture(const class renderer& renderer, const texture_args& args); + + /// Creates a texture with the specified parameters. + /// \param renderer Renderer to create the texture for. + /// \param format Pixel format. + /// \param size Texture dimensions. + /// \param access Access pattern. + /// \throws laya::error if texture creation fails. + texture(const class renderer& renderer, pixel_format format, dimentions size, + texture_access access = texture_access::static_); + + /// Creates a texture from a surface. + /// \param renderer Renderer to create the texture for. + /// \param surf Surface to create texture from. + /// \throws laya::error if texture creation fails. + [[nodiscard]] static texture from_surface(const class renderer& renderer, const surface& surf); + + /// Loads a texture from a BMP file. + /// \param renderer Renderer to create the texture for. + /// \param path Path to BMP file. + /// \returns Loaded texture. + /// \throws laya::error if loading fails. + [[nodiscard]] static texture load_bmp(const class renderer& renderer, std::string_view path); + + /// Loads a texture from a PNG file (requires SDL_image). + /// \param renderer Renderer to create the texture for. + /// \param path Path to PNG file. + /// \returns Loaded texture. + /// \throws laya::error if loading fails. + [[nodiscard]] static texture load_png(const class renderer& renderer, std::string_view path); + + /// Destroys the texture and releases resources. + ~texture() noexcept; + + // Non-copyable but movable + texture(const texture&) = delete; + texture& operator=(const texture&) = delete; + texture(texture&& other) noexcept; + texture& operator=(texture&& other) noexcept; + + // ======================================================================== + // State management + // ======================================================================== + + /// Sets alpha modulation for the texture. + /// \param alpha Alpha value (0-255). + /// \throws laya::error on SDL failure. + void set_alpha_mod(std::uint8_t alpha); + + /// Sets alpha modulation for the texture using normalized float (0.0 - 1.0). + /// \param alpha Alpha value (0.0-1.0). + /// \throws laya::error on SDL failure. + void set_alpha_mod(float alpha); + + /// Sets color modulation for the texture. + /// \param c Color modulation. + /// \throws laya::error on SDL failure. + void set_color_mod(color c); + + /// Sets color modulation for the texture. + /// \param r Red component (0-255). + /// \param g Green component (0-255). + /// \param b Blue component (0-255). + /// \throws laya::error on SDL failure. + void set_color_mod(std::uint8_t r, std::uint8_t g, std::uint8_t b); + + /// Sets color modulation for the texture using normalized floats (0.0 - 1.0). + /// \param r Red component (0.0-1.0). + /// \param g Green component (0.0-1.0). + /// \param b Blue component (0.0-1.0). + /// \throws laya::error on SDL failure. + void set_color_mod(float r, float g, float b); + + /// Sets blend mode for the texture. + /// \param mode Blend mode to use. + /// \throws laya::error on SDL failure. + void set_blend_mode(blend_mode mode); + + /// Sets scale mode for the texture. + /// \param mode Scaling mode to use. + /// \throws laya::error on SDL failure. + void set_scale_mode(scale_mode mode); + + /// Gets current alpha modulation. + /// \returns Alpha value (0-255). + /// \throws laya::error on SDL failure. + [[nodiscard]] std::uint8_t get_alpha_mod() const; + + /// Gets current alpha modulation as normalized float. + /// \returns Alpha value (0.0-1.0). + /// \throws laya::error on SDL failure. + [[nodiscard]] float get_alpha_mod_float() const; + + /// Gets current color modulation. + /// \returns Color modulation values. + /// \throws laya::error on SDL failure. + [[nodiscard]] color get_color_mod() const; + + /// Gets current color modulation as normalized floats. + /// \returns Color modulation values (0.0-1.0). + /// \throws laya::error on SDL failure. + [[nodiscard]] color_f get_color_mod_float() const; + + /// Gets current blend mode. + /// \returns Current blend mode. + /// \throws laya::error on SDL failure. + [[nodiscard]] blend_mode get_blend_mode() const; + + /// Gets current scale mode. + /// \returns Current scale mode. + /// \throws laya::error on SDL failure. + [[nodiscard]] scale_mode get_scale_mode() const; + + // ======================================================================== + // Pixel access and updates + // ======================================================================== + + /// Updates texture pixels from raw data. + /// \param pixels Pixel data to upload. + /// \param pitch Bytes per row. + /// \throws laya::error on SDL failure. + void update(const void* pixels, int pitch); + + /// Updates a rectangular region of texture pixels. + /// \param r Rectangle to update. + /// \param pixels Pixel data to upload. + /// \param pitch Bytes per row. + /// \throws laya::error on SDL failure. + void update(const rect& r, const void* pixels, int pitch); + void update(const surface& surf); + + /// Locks the texture for direct pixel access. + /// \returns Lock guard that automatically unlocks on destruction. + /// \throws laya::error if the texture cannot be locked. + texture_lock_guard lock(); + + /// Locks a rectangular region for direct pixel access. + /// \param r Rectangle to lock. + /// \returns Lock guard that automatically unlocks on destruction. + /// \throws laya::error if the texture cannot be locked. + texture_lock_guard lock(const rect& r); + + // ======================================================================== + // Query methods + // ======================================================================== + + /// Gets texture dimensions. + /// \returns Width and height of the texture. + [[nodiscard]] dimentions size() const noexcept; + + /// Gets texture pixel format. + /// \returns Pixel format of the texture. + [[nodiscard]] pixel_format format() const noexcept; + + /// Gets texture access pattern. + /// \returns Access pattern of the texture. + [[nodiscard]] texture_access access() const noexcept; + + /// Gets the native SDL texture handle. + /// \returns SDL_Texture pointer for direct SDL operations. + [[nodiscard]] SDL_Texture* native_handle() const noexcept; + +private: + SDL_Texture* m_texture{nullptr}; + dimentions m_size{0, 0}; + pixel_format m_format{pixel_format::rgba32}; + texture_access m_access{texture_access::static_}; + + /// Private constructor for factory methods. + explicit texture(SDL_Texture* tex); + + friend class texture_lock_guard; +}; + +} // namespace laya diff --git a/include/laya/textures/texture_access.hpp b/include/laya/textures/texture_access.hpp new file mode 100644 index 0000000..5ca2dab --- /dev/null +++ b/include/laya/textures/texture_access.hpp @@ -0,0 +1,36 @@ +/// Texture access patterns and scaling modes for SDL3 texture operations. +/// \file texture_access.hpp +/// \date 2025-09-28 + +#pragma once + +#include + +#include + +namespace laya { + +/// Texture access patterns define how textures can be used. +/// Maps directly to SDL_TextureAccess values for type safety. +enum class texture_access : int { + /// Texture changes rarely, not lockable. + static_ = SDL_TEXTUREACCESS_STATIC, + + /// Texture changes frequently, lockable. + streaming = SDL_TEXTUREACCESS_STREAMING, + + /// Texture can be used as a render target. + target = SDL_TEXTUREACCESS_TARGET +}; + +/// Texture scaling modes for rendering operations. +/// Maps directly to SDL_ScaleMode values for type safety. +enum class scale_mode : int { + /// Nearest pixel sampling. + nearest = SDL_SCALEMODE_NEAREST, + + /// Linear filtering. + linear = SDL_SCALEMODE_LINEAR +}; + +} // namespace laya diff --git a/mkdocs.yml b/mkdocs.yml index 3c56ec0..14c7212 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -67,6 +67,8 @@ nav: - Features: - Windows: features/windows.md - Rendering: features/rendering.md + - Surfaces: features/surfaces.md + - Textures: features/textures.md - Events: features/events.md - Logging: features/logging.md diff --git a/scripts/README.md b/scripts/README.md index 1484092..1299e34 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -34,6 +34,35 @@ The script will: - Start the MkDocs development server with live reload - Watch for changes and auto-refresh the browser +## run-pre-commit.sh + +Runs pre-commit hooks for the project using uv and the tools environment. This is a convenient wrapper around the pre-commit command referenced in the project's AGENTS.md file. + +```bash +# Run all hooks on all files (default) +./scripts/run-pre-commit.sh + +# Run hooks only on specific files +./scripts/run-pre-commit.sh --files src/laya/window.cpp + +# Run hooks for push stage +./scripts/run-pre-commit.sh --hook-stage push + +# Show diffs when hooks fail +./scripts/run-pre-commit.sh --show-diff-on-failure + +# Get help and see all options +./scripts/run-pre-commit.sh --help +``` + +The script will: + +- Execute `uv run --project tools --group dev pre-commit run` with provided arguments +- Default to `--all-files` when no arguments are given +- Provide colored output for better readability +- Pass through all pre-commit flags and options +- Exit with the same code as the underlying pre-commit command + ## run-actions.sh ```bash diff --git a/scripts/run-pre-commit.sh b/scripts/run-pre-commit.sh new file mode 100755 index 0000000..e4d9189 --- /dev/null +++ b/scripts/run-pre-commit.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +# run-pre-commit.sh +# +# Runs pre-commit hooks for the project using uv and the tools environment. +# By default runs all hooks on all files, but can pass additional flags to pre-commit. +# +# Usage: +# ./scripts/run-pre-commit.sh # Run all hooks on all files +# ./scripts/run-pre-commit.sh --files file.py # Run hooks only on specific files +# ./scripts/run-pre-commit.sh --hook-stage push # Run hooks for push stage +# ./scripts/run-pre-commit.sh -h # Show this help + +# Color helpers for TTY output +if [[ -t 1 ]]; then + RED="\e[31m"; GREEN="\e[32m"; YELLOW="\e[33m"; CYAN="\e[36m"; MAGENTA="\e[35m"; RESET="\e[0m" +else + RED=""; GREEN=""; YELLOW=""; CYAN=""; MAGENTA=""; RESET="" +fi + +info() { printf "%b[INFO] %s%b\n" "$CYAN" "$*" "$RESET"; } +success() { printf "%b[SUCCESS] %s%b\n" "$GREEN" "$*" "$RESET"; } +warn() { printf "%b[WARN] %s%b\n" "$YELLOW" "$*" "$RESET"; } +error() { printf "%b[ERROR] %s%b\n" "$RED" "$*" "$RESET"; } + +usage() { + cat << EOF +run-pre-commit.sh - Run pre-commit hooks for the laya project + +SYNOPSIS + ./scripts/run-pre-commit.sh [OPTIONS...] + +DESCRIPTION + Executes pre-commit hooks using uv and the tools environment. By default, + runs all hooks on all files. Additional flags are passed through to the + pre-commit command. + +OPTIONS + -h, --help Show this help message + + All other options are passed directly to pre-commit. Common options include: + --files FILE... Run hooks only on specified files + --hook-stage STAGE Run hooks for specific stage (commit, push, etc.) + --all-files Run on all files (default when no args provided) + --show-diff-on-failure Show diff when hooks fail + +EXAMPLES + # Run all hooks on all files (default) + ./scripts/run-pre-commit.sh + + # Run hooks only on specific files + ./scripts/run-pre-commit.sh --files src/laya/window.cpp + + # Run hooks for push stage + ./scripts/run-pre-commit.sh --hook-stage push + + # Show diffs when hooks fail + ./scripts/run-pre-commit.sh --show-diff-on-failure + +EOF +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + usage + exit 0 + ;; + *) + # All other arguments are passed to pre-commit + break + ;; + esac +done + +# If no arguments provided, default to --all-files +if [[ $# -eq 0 ]]; then + set -- --all-files +fi + +# Check if we're in the project root (look for pyproject.toml in tools/) +if [[ ! -f "tools/pyproject.toml" ]]; then + error "This script must be run from the project root directory" + error "Expected to find tools/pyproject.toml in the current directory" + exit 1 +fi + +# Show what we're about to do +info "Running pre-commit hooks with: uv run --project tools --group dev pre-commit run $*" + +# Execute pre-commit via uv +if uv run --project tools --group dev pre-commit run "$@"; then + success "Pre-commit hooks completed successfully" +else + exit_code=$? + error "Pre-commit hooks failed (exit code: $exit_code)" + exit $exit_code +fi diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dba6257..ceeba2f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,5 +9,7 @@ target_sources( laya/event_types.cpp laya/event_polling.cpp laya/renderer.cpp + laya/surface.cpp + laya/texture.cpp laya/log.cpp ) diff --git a/src/laya/renderer.cpp b/src/laya/renderer.cpp index e5f0fe2..05a9afc 100644 --- a/src/laya/renderer.cpp +++ b/src/laya/renderer.cpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace laya { @@ -338,6 +339,96 @@ void renderer::fill_rects(const rect* rects, int count) { } } +// ============================================================================ +// Texture rendering operations +// ============================================================================ + +void renderer::render(const texture& tex, point dst_pos) { + auto tex_size = tex.size(); + SDL_FRect dst_rect{static_cast(dst_pos.x), static_cast(dst_pos.y), static_cast(tex_size.width), + static_cast(tex_size.height)}; + + if (!SDL_RenderTexture(m_renderer, tex.native_handle(), nullptr, &dst_rect)) { + throw error("Failed to render texture: {}", SDL_GetError()); + } +} + +void renderer::render(const texture& tex, const rect& dst_rect) { + SDL_FRect sdl_dst{static_cast(dst_rect.x), static_cast(dst_rect.y), static_cast(dst_rect.w), + static_cast(dst_rect.h)}; + + if (!SDL_RenderTexture(m_renderer, tex.native_handle(), nullptr, &sdl_dst)) { + throw error("Failed to render texture: {}", SDL_GetError()); + } +} + +void renderer::render(const texture& tex, const rect& src_rect, const rect& dst_rect) { + SDL_FRect sdl_src{static_cast(src_rect.x), static_cast(src_rect.y), static_cast(src_rect.w), + static_cast(src_rect.h)}; + SDL_FRect sdl_dst{static_cast(dst_rect.x), static_cast(dst_rect.y), static_cast(dst_rect.w), + static_cast(dst_rect.h)}; + + if (!SDL_RenderTexture(m_renderer, tex.native_handle(), &sdl_src, &sdl_dst)) { + throw error("Failed to render texture: {}", SDL_GetError()); + } +} + +void renderer::render(const texture& tex, const rect& dst_rect, double angle) { + SDL_FRect sdl_dst{static_cast(dst_rect.x), static_cast(dst_rect.y), static_cast(dst_rect.w), + static_cast(dst_rect.h)}; + + if (!SDL_RenderTextureRotated(m_renderer, tex.native_handle(), nullptr, &sdl_dst, angle, nullptr, SDL_FLIP_NONE)) { + throw error("Failed to render rotated texture: {}", SDL_GetError()); + } +} + +void renderer::render(const texture& tex, const rect& dst_rect, double angle, point center) { + SDL_FRect sdl_dst{static_cast(dst_rect.x), static_cast(dst_rect.y), static_cast(dst_rect.w), + static_cast(dst_rect.h)}; + SDL_FPoint sdl_center{static_cast(center.x), static_cast(center.y)}; + + if (!SDL_RenderTextureRotated(m_renderer, tex.native_handle(), nullptr, &sdl_dst, angle, &sdl_center, + SDL_FLIP_NONE)) { + throw error("Failed to render rotated texture: {}", SDL_GetError()); + } +} + +void renderer::render(const texture& tex, const rect& src_rect, const rect& dst_rect, double angle, point center, + flip_mode flip) { + SDL_FRect sdl_src{static_cast(src_rect.x), static_cast(src_rect.y), static_cast(src_rect.w), + static_cast(src_rect.h)}; + SDL_FRect sdl_dst{static_cast(dst_rect.x), static_cast(dst_rect.y), static_cast(dst_rect.w), + static_cast(dst_rect.h)}; + SDL_FPoint sdl_center{static_cast(center.x), static_cast(center.y)}; + + if (!SDL_RenderTextureRotated(m_renderer, tex.native_handle(), &sdl_src, &sdl_dst, angle, &sdl_center, + static_cast(flip))) { + throw error("Failed to render rotated texture: {}", SDL_GetError()); + } +} + +void renderer::render(const texture& tex, const rect& dst_rect, flip_mode flip) { + SDL_FRect sdl_dst{static_cast(dst_rect.x), static_cast(dst_rect.y), static_cast(dst_rect.w), + static_cast(dst_rect.h)}; + + if (!SDL_RenderTextureRotated(m_renderer, tex.native_handle(), nullptr, &sdl_dst, 0.0, nullptr, + static_cast(flip))) { + throw error("Failed to render flipped texture: {}", SDL_GetError()); + } +} + +void renderer::render(const texture& tex, const rect& src_rect, const rect& dst_rect, flip_mode flip) { + SDL_FRect sdl_src{static_cast(src_rect.x), static_cast(src_rect.y), static_cast(src_rect.w), + static_cast(src_rect.h)}; + SDL_FRect sdl_dst{static_cast(dst_rect.x), static_cast(dst_rect.y), static_cast(dst_rect.w), + static_cast(dst_rect.h)}; + + if (!SDL_RenderTextureRotated(m_renderer, tex.native_handle(), &sdl_src, &sdl_dst, 0.0, nullptr, + static_cast(flip))) { + throw error("Failed to render flipped texture: {}", SDL_GetError()); + } +} + // ============================================================================ // RAII state guard implementations // ============================================================================ diff --git a/src/laya/surface.cpp b/src/laya/surface.cpp new file mode 100644 index 0000000..d4db81f --- /dev/null +++ b/src/laya/surface.cpp @@ -0,0 +1,350 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std::string_view_literals; + +namespace laya { + +// ============================================================================ +// surface_lock_guard implementation +// ============================================================================ + +surface_lock_guard::surface_lock_guard(class surface& surf) : m_surface(&surf) { + if (!SDL_LockSurface(surf.native_handle())) { + throw error::from_sdl(); + } + m_pixels = surf.native_handle()->pixels; + m_pitch = surf.native_handle()->pitch; +} + +surface_lock_guard::surface_lock_guard(surface_lock_guard&& other) noexcept + : m_surface{std::exchange(other.m_surface, nullptr)}, m_pixels{other.m_pixels}, m_pitch{other.m_pitch} { +} + +surface_lock_guard& surface_lock_guard::operator=(surface_lock_guard&& other) noexcept { + if (this != &other) { + if (m_surface) { + SDL_UnlockSurface(m_surface->native_handle()); + } + m_surface = std::exchange(other.m_surface, nullptr); + m_pixels = other.m_pixels; + m_pitch = other.m_pitch; + } + return *this; +} + +surface_lock_guard::~surface_lock_guard() noexcept { + if (m_surface) { + SDL_UnlockSurface(m_surface->native_handle()); + } +} + +void* surface_lock_guard::pixels() const noexcept { + return m_pixels; +} + +int surface_lock_guard::pitch() const noexcept { + return m_pitch; +} + +// ============================================================================ +// surface implementation +// ============================================================================ + +surface::surface(const surface_args& args) { + if ((args.flags & surface_flags::preallocated) == surface_flags::preallocated) { + throw std::runtime_error( + "surface_flags::preallocated is not supported yet; supply external pixel memory before enabling this flag"); + } + + m_surface = SDL_CreateSurface(args.size.width, args.size.height, static_cast(args.format)); + + if (!m_surface) { + throw error::from_sdl(); + } + + if ((args.flags & surface_flags::rle_optimized) == surface_flags::rle_optimized) { + if (!SDL_SetSurfaceRLE(m_surface, true)) { + throw error::from_sdl(); + } + } +} + +surface::surface(dimentions size, pixel_format format) { + m_surface = SDL_CreateSurface(size.width, size.height, static_cast(format)); + + if (!m_surface) { + throw error::from_sdl(); + } +} + +surface surface::load_bmp(std::string_view path) { + SDL_Surface* surf = SDL_LoadBMP(std::string(path).c_str()); + if (!surf) { + throw error::from_sdl(); + } + return surface(surf); +} + +surface surface::load_png(std::string_view path) { + // Note: This requires SDL_image + // SDL_Surface* surf = IMG_Load(std::string(path).c_str()); + // For now, throw an error indicating PNG support requires SDL_image + (void)path; // Mark parameter as used + throw error("PNG loading requires SDL_image library - not yet implemented: {}", "feature not available"); +} + +surface::~surface() noexcept { + if (m_surface) { + SDL_DestroySurface(m_surface); + } +} + +surface::surface(surface&& other) noexcept : m_surface{std::exchange(other.m_surface, nullptr)} { +} + +surface& surface::operator=(surface&& other) noexcept { + if (this != &other) { + if (m_surface) { + SDL_DestroySurface(m_surface); + } + m_surface = std::exchange(other.m_surface, nullptr); + } + return *this; +} + +void surface::fill(color c) { + const std::uint32_t mapped_color = SDL_MapSurfaceRGBA(m_surface, c.r, c.g, c.b, c.a); + + if (!SDL_FillSurfaceRect(m_surface, nullptr, mapped_color)) { + throw error::from_sdl(); + } +} + +void surface::fill_rect(const rect& r, color c) { + const SDL_Rect sdl_rect{r.x, r.y, r.w, r.h}; + const std::uint32_t mapped_color = SDL_MapSurfaceRGBA(m_surface, c.r, c.g, c.b, c.a); + + if (!SDL_FillSurfaceRect(m_surface, &sdl_rect, mapped_color)) { + throw error::from_sdl(); + } +} + +void surface::fill_rects(std::span rects, color c) { + const std::uint32_t mapped_color = SDL_MapSurfaceRGBA(m_surface, c.r, c.g, c.b, c.a); + + // Convert laya rects to SDL rects + std::vector sdl_rects; + sdl_rects.reserve(rects.size()); + + std::transform(rects.begin(), rects.end(), std::back_inserter(sdl_rects), + [](const rect& r) { return SDL_Rect{r.x, r.y, r.w, r.h}; }); + + if (!SDL_FillSurfaceRects(m_surface, sdl_rects.data(), static_cast(sdl_rects.size()), mapped_color)) { + throw error::from_sdl(); + } +} + +void surface::clear() { + if (!SDL_ClearSurface(m_surface, 0.0f, 0.0f, 0.0f, 0.0f)) { + throw error::from_sdl(); + } +} + +void surface::blit(const surface& src, const rect& src_rect, const rect& dst_rect) { + const SDL_Rect sdl_src_rect{src_rect.x, src_rect.y, src_rect.w, src_rect.h}; + const SDL_Rect sdl_dst_rect{dst_rect.x, dst_rect.y, dst_rect.w, dst_rect.h}; + + if (!SDL_BlitSurface(src.m_surface, &sdl_src_rect, m_surface, &sdl_dst_rect)) { + throw error::from_sdl(); + } +} + +void surface::blit(const surface& src, point dst_pos) { + SDL_Rect sdl_dst_rect{dst_pos.x, dst_pos.y, 0, 0}; + + if (!SDL_BlitSurface(src.m_surface, nullptr, m_surface, &sdl_dst_rect)) { + throw error::from_sdl(); + } +} + +void surface::set_alpha_mod(std::uint8_t alpha) { + if (!SDL_SetSurfaceAlphaMod(m_surface, alpha)) { + throw error::from_sdl(); + } +} + +void surface::set_color_mod(color c) { + if (!SDL_SetSurfaceColorMod(m_surface, c.r, c.g, c.b)) { + throw error::from_sdl(); + } +} + +void surface::set_color_mod(std::uint8_t r, std::uint8_t g, std::uint8_t b) { + if (!SDL_SetSurfaceColorMod(m_surface, r, g, b)) { + throw error::from_sdl(); + } +} + +void surface::set_blend_mode(blend_mode mode) { + if (!SDL_SetSurfaceBlendMode(m_surface, static_cast(mode))) { + throw error::from_sdl(); + } +} + +void surface::set_color_key(color key) { + const std::uint32_t mapped_key = SDL_MapSurfaceRGBA(m_surface, key.r, key.g, key.b, key.a); + if (!SDL_SetSurfaceColorKey(m_surface, true, mapped_key)) { + throw error::from_sdl(); + } +} + +void surface::clear_color_key() { + if (!SDL_SetSurfaceColorKey(m_surface, false, 0)) { + throw error::from_sdl(); + } +} + +std::uint8_t surface::get_alpha_mod() const { + std::uint8_t alpha; + if (!SDL_GetSurfaceAlphaMod(m_surface, &alpha)) { + throw error::from_sdl(); + } + return alpha; +} + +color surface::get_color_mod() const { + std::uint8_t r, g, b; + if (!SDL_GetSurfaceColorMod(m_surface, &r, &g, &b)) { + throw error::from_sdl(); + } + return color{r, g, b}; +} + +blend_mode surface::get_blend_mode() const { + SDL_BlendMode mode; + if (!SDL_GetSurfaceBlendMode(m_surface, &mode)) { + throw error::from_sdl(); + } + return static_cast(mode); +} + +bool surface::has_color_key() const { + return SDL_SurfaceHasColorKey(m_surface); +} + +color surface::get_color_key() const { + std::uint32_t key; + if (!SDL_GetSurfaceColorKey(m_surface, &key)) { + throw error::from_sdl(); + } + + auto* format_details = SDL_GetPixelFormatDetails(m_surface->format); + if (!format_details) { + throw error::from_sdl(); + } + + std::uint8_t r{}, g{}, b{}, a{}; + SDL_GetRGBA(key, format_details, nullptr, &r, &g, &b, &a); + return color{r, g, b, a}; +} + +dimentions surface::size() const noexcept { + return {m_surface->w, m_surface->h}; +} + +pixel_format surface::format() const noexcept { + return static_cast(m_surface->format); +} + +bool surface::must_lock() const noexcept { + // SDL3 surfaces typically do not require locking; retain compatibility hook + return false; +} + +surface surface::convert(pixel_format format) const { + SDL_Surface* converted = SDL_ConvertSurface(m_surface, static_cast(format)); + if (!converted) { + throw error::from_sdl(); + } + return surface(converted); +} + +surface surface::duplicate() const { + SDL_Surface* duplicated = SDL_DuplicateSurface(m_surface); + if (!duplicated) { + throw error::from_sdl(); + } + return surface(duplicated); +} + +surface surface::scale(dimentions new_size) const { + SDL_Surface* scaled = SDL_ScaleSurface(m_surface, new_size.width, new_size.height, SDL_SCALEMODE_LINEAR); + if (!scaled) { + throw error::from_sdl(); + } + return surface(scaled); +} + +surface surface::flip(flip_mode mode) const { + // Create a duplicate first, then flip it in place + SDL_Surface* flipped = SDL_DuplicateSurface(m_surface); + if (!flipped) { + throw error::from_sdl(); + } + + SDL_FlipMode sdl_flip = SDL_FLIP_NONE; + switch (mode) { + case flip_mode::horizontal: + sdl_flip = SDL_FLIP_HORIZONTAL; + break; + case flip_mode::vertical: + sdl_flip = SDL_FLIP_VERTICAL; + break; + case flip_mode::none: + default: + // No flipping needed, just return the duplicate + return surface(flipped); + } + + if (!SDL_FlipSurface(flipped, sdl_flip)) { + SDL_DestroySurface(flipped); + throw error::from_sdl(); + } + return surface(flipped); +} + +surface_lock_guard surface::lock() { + return surface_lock_guard(*this); +} + +void surface::save_bmp(std::string_view path) const { + if (!SDL_SaveBMP(m_surface, std::string(path).c_str())) { + throw error::from_sdl(); + } +} + +void surface::save_png(std::string_view path) const { + // Note: PNG saving requires SDL_image + // IMG_SavePNG(m_surface, std::string(path).c_str()) + (void)path; // Mark parameter as used + throw error("PNG saving requires SDL_image library - not yet implemented: {}", "feature not available"); +} + +SDL_Surface* surface::native_handle() const noexcept { + return m_surface; +} + +surface::surface(SDL_Surface* surf) : m_surface(surf) { + // Private constructor for factory methods - takes ownership +} + +} // namespace laya diff --git a/src/laya/texture.cpp b/src/laya/texture.cpp new file mode 100644 index 0000000..0c8bbbf --- /dev/null +++ b/src/laya/texture.cpp @@ -0,0 +1,268 @@ +#include +#include +#include + +#include +#include +#include + +using namespace std::string_view_literals; + +namespace laya { + +// ============================================================================ +// texture_lock_guard implementation +// ============================================================================ + +texture_lock_guard::texture_lock_guard(class texture& tex, const rect* region) : m_texture(&tex) { + SDL_Rect sdl_rect{}; + SDL_Rect* rect_ptr = nullptr; + if (region) { + sdl_rect = SDL_Rect{region->x, region->y, region->w, region->h}; + rect_ptr = &sdl_rect; + } + + if (!SDL_LockTexture(tex.native_handle(), rect_ptr, &m_pixels, &m_pitch)) { + throw error::from_sdl(); + } +} + +texture_lock_guard::texture_lock_guard(texture_lock_guard&& other) noexcept + : m_texture{std::exchange(other.m_texture, nullptr)}, m_pixels{other.m_pixels}, m_pitch{other.m_pitch} { +} + +texture_lock_guard& texture_lock_guard::operator=(texture_lock_guard&& other) noexcept { + if (this != &other) { + if (m_texture) { + SDL_UnlockTexture(m_texture->native_handle()); + } + m_texture = std::exchange(other.m_texture, nullptr); + m_pixels = other.m_pixels; + m_pitch = other.m_pitch; + } + return *this; +} + +texture_lock_guard::~texture_lock_guard() noexcept { + if (m_texture) { + SDL_UnlockTexture(m_texture->native_handle()); + } +} + +void* texture_lock_guard::pixels() const noexcept { + return m_pixels; +} + +int texture_lock_guard::pitch() const noexcept { + return m_pitch; +} + +// ============================================================================ +// texture implementation +// ============================================================================ + +texture::texture(const class renderer& renderer, const texture_args& args) + : texture(renderer, args.format, args.size, args.access) { +} + +texture::texture(const class renderer& renderer, pixel_format format, dimentions size, texture_access access) + : m_size{size}, m_format{format}, m_access{access} { + m_texture = SDL_CreateTexture(renderer.native_handle(), static_cast(format), + static_cast(access), size.width, size.height); + + if (!m_texture) { + throw error::from_sdl(); + } +} + +texture texture::from_surface(const class renderer& renderer, const surface& surf) { + SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer.native_handle(), surf.native_handle()); + if (!tex) { + throw error::from_sdl(); + } + + texture result(tex); + SDL_PropertiesID props = SDL_GetTextureProperties(tex); + if (props) { + result.m_size.width = SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_WIDTH_NUMBER, surf.size().width); + result.m_size.height = SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_HEIGHT_NUMBER, surf.size().height); + result.m_format = static_cast( + SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_FORMAT_NUMBER, static_cast(surf.format()))); + result.m_access = static_cast( + SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_ACCESS_NUMBER, SDL_TEXTUREACCESS_STATIC)); + } else { + result.m_size = surf.size(); + result.m_format = surf.format(); + result.m_access = texture_access::static_; + } + + return result; +} + +texture texture::load_bmp(const class renderer& renderer, std::string_view path) { + // Load surface first, then convert to texture + auto surf = surface::load_bmp(path); + return from_surface(renderer, surf); +} + +texture texture::load_png(const class renderer& renderer, std::string_view path) { + // Load surface first, then convert to texture + auto surf = surface::load_png(path); // This will throw since PNG isn't implemented yet + return from_surface(renderer, surf); +} + +texture::~texture() noexcept { + if (m_texture) { + SDL_DestroyTexture(m_texture); + } +} + +texture::texture(texture&& other) noexcept + : m_texture{std::exchange(other.m_texture, nullptr)}, + m_size{std::exchange(other.m_size, dimentions{0, 0})}, + m_format{std::exchange(other.m_format, pixel_format::rgba32)}, + m_access{std::exchange(other.m_access, texture_access::static_)} { +} + +texture& texture::operator=(texture&& other) noexcept { + if (this != &other) { + if (m_texture) { + SDL_DestroyTexture(m_texture); + } + m_texture = std::exchange(other.m_texture, nullptr); + m_size = std::exchange(other.m_size, dimentions{0, 0}); + m_format = std::exchange(other.m_format, pixel_format::rgba32); + m_access = std::exchange(other.m_access, texture_access::static_); + } + return *this; +} + +void texture::update(const void* pixels, int pitch) { + if (!SDL_UpdateTexture(m_texture, nullptr, pixels, pitch)) { + throw error::from_sdl(); + } +} + +void texture::update(const rect& r, const void* pixels, int pitch) { + const SDL_Rect sdl_rect{r.x, r.y, r.w, r.h}; + if (!SDL_UpdateTexture(m_texture, &sdl_rect, pixels, pitch)) { + throw error::from_sdl(); + } +} + +void texture::update(const surface& surf) { + if (!SDL_UpdateTexture(m_texture, nullptr, surf.native_handle()->pixels, surf.native_handle()->pitch)) { + throw error::from_sdl(); + } +} + +texture_lock_guard texture::lock() { + return texture_lock_guard(*this, nullptr); +} + +texture_lock_guard texture::lock(const rect& r) { + return texture_lock_guard(*this, &r); +} + +void texture::set_alpha_mod(std::uint8_t alpha) { + if (!SDL_SetTextureAlphaMod(m_texture, alpha)) { + throw error::from_sdl(); + } +} + +void texture::set_alpha_mod(float alpha) { + set_alpha_mod(static_cast(alpha * 255.0f)); +} + +void texture::set_color_mod(color c) { + if (!SDL_SetTextureColorMod(m_texture, c.r, c.g, c.b)) { + throw error::from_sdl(); + } +} + +void texture::set_color_mod(std::uint8_t r, std::uint8_t g, std::uint8_t b) { + if (!SDL_SetTextureColorMod(m_texture, r, g, b)) { + throw error::from_sdl(); + } +} + +void texture::set_color_mod(float r, float g, float b) { + set_color_mod(static_cast(r * 255.0f), static_cast(g * 255.0f), + static_cast(b * 255.0f)); +} + +void texture::set_blend_mode(blend_mode mode) { + if (!SDL_SetTextureBlendMode(m_texture, static_cast(mode))) { + throw error::from_sdl(); + } +} + +void texture::set_scale_mode(scale_mode mode) { + if (!SDL_SetTextureScaleMode(m_texture, static_cast(mode))) { + throw error::from_sdl(); + } +} + +float texture::get_alpha_mod_float() const { + return static_cast(get_alpha_mod()) / 255.0f; +} + +color_f texture::get_color_mod_float() const { + auto c = get_color_mod(); + return color_f{static_cast(c.r) / 255.0f, static_cast(c.g) / 255.0f, static_cast(c.b) / 255.0f, + 1.0f}; +} + +std::uint8_t texture::get_alpha_mod() const { + std::uint8_t alpha; + if (!SDL_GetTextureAlphaMod(m_texture, &alpha)) { + throw error::from_sdl(); + } + return alpha; +} + +color texture::get_color_mod() const { + std::uint8_t r, g, b; + if (!SDL_GetTextureColorMod(m_texture, &r, &g, &b)) { + throw error::from_sdl(); + } + return color{r, g, b}; +} + +blend_mode texture::get_blend_mode() const { + SDL_BlendMode mode; + if (!SDL_GetTextureBlendMode(m_texture, &mode)) { + throw error::from_sdl(); + } + return static_cast(mode); +} + +scale_mode texture::get_scale_mode() const { + SDL_ScaleMode mode; + if (!SDL_GetTextureScaleMode(m_texture, &mode)) { + throw error::from_sdl(); + } + return static_cast(mode); +} + +dimentions texture::size() const noexcept { + return m_size; +} + +pixel_format texture::format() const noexcept { + return m_format; +} + +texture_access texture::access() const noexcept { + return m_access; +} + +SDL_Texture* texture::native_handle() const noexcept { + return m_texture; +} + +texture::texture(SDL_Texture* tex) : m_texture(tex) { + // Private constructor for factory methods - takes ownership +} + +} // namespace laya diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9495c6d..8c43412 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,6 +39,7 @@ if(LAYA_TESTS_UNIT) unit/test_event_range.cpp unit/test_window_event.cpp unit/test_logging.cpp + unit/test_surface.cpp ) # Create unit test executable diff --git a/tests/unit/test_surface.cpp b/tests/unit/test_surface.cpp new file mode 100644 index 0000000..cb5356a --- /dev/null +++ b/tests/unit/test_surface.cpp @@ -0,0 +1,161 @@ +/// @file test_surface.cpp +/// @brief Basic unit tests for surface API +/// @date 2025-12-10 + +#include +#include + +using namespace laya; + +TEST_SUITE("Surface") { + // Helper function to create a test surface + auto create_test_surface = [](dimentions size = {64, 64}, pixel_format fmt = pixel_format::rgba32) { + return surface{size, fmt}; + }; + + TEST_CASE("Surface creation - Basic surface creation") { + laya::context ctx{laya::subsystem::video}; + auto surf = create_test_surface(); + + CHECK(surf.size().width == 64); + CHECK(surf.size().height == 64); + CHECK(surf.format() == pixel_format::rgba32); + CHECK(surf.native_handle() != nullptr); + } + + TEST_CASE("Surface creation - Surface creation with args") { + laya::context ctx{laya::subsystem::video}; + surface_args args{.size = {128, 256}, .format = pixel_format::bgra32, .flags = surface_flags::rle_optimized}; + + laya::surface surf{args}; + + CHECK(surf.size().width == 128); + CHECK(surf.size().height == 256); + CHECK(surf.format() == pixel_format::bgra32); + } + + TEST_CASE("Surface operations - Fill operations") { + laya::context ctx{laya::subsystem::video}; + auto surf = create_test_surface({32, 32}); + + // These should not throw + CHECK_NOTHROW(surf.fill(laya::colors::red)); + CHECK_NOTHROW(surf.fill_rect({0, 0, 16, 16}, laya::colors::blue)); + CHECK_NOTHROW(surf.clear()); + } + + TEST_CASE("Surface operations - State management") { + laya::context ctx{laya::subsystem::video}; + auto surf = create_test_surface({32, 32}); + + // These should not throw + CHECK_NOTHROW(surf.set_alpha_mod(128)); + CHECK_NOTHROW(surf.set_color_mod(laya::colors::green)); + CHECK_NOTHROW(surf.set_blend_mode(laya::blend_mode::blend)); + + // Verify getters work + CHECK(surf.get_alpha_mod() == 128); + CHECK(surf.get_color_mod() == laya::colors::green); + CHECK(surf.get_blend_mode() == laya::blend_mode::blend); + } + + TEST_CASE("Surface operations - Color key operations") { + laya::context ctx{laya::subsystem::video}; + auto surf = create_test_surface({32, 32}); + + CHECK_FALSE(surf.has_color_key()); + + CHECK_NOTHROW(surf.set_color_key(laya::colors::magenta)); + CHECK(surf.has_color_key()); + CHECK(surf.get_color_key() == laya::colors::magenta); + + CHECK_NOTHROW(surf.clear_color_key()); + CHECK_FALSE(surf.has_color_key()); + } + + TEST_CASE("Surface transformations - Duplicate") { + laya::context ctx{laya::subsystem::video}; + auto surf = create_test_surface({16, 16}); + + auto dup = surf.duplicate(); + CHECK(dup.size().width == surf.size().width); + CHECK(dup.size().height == surf.size().height); + CHECK(dup.format() == surf.format()); + CHECK(dup.native_handle() != surf.native_handle()); // Different objects + } + + TEST_CASE("Surface transformations - Convert format") { + laya::context ctx{laya::subsystem::video}; + auto surf = create_test_surface({16, 16}); + + auto converted = surf.convert(pixel_format::bgra32); + CHECK(converted.size().width == surf.size().width); + CHECK(converted.size().height == surf.size().height); + CHECK(converted.format() == pixel_format::bgra32); + } + + TEST_CASE("Surface transformations - Scale") { + laya::context ctx{laya::subsystem::video}; + auto surf = create_test_surface({16, 16}); + + auto scaled = surf.scale({32, 32}); + CHECK(scaled.size().width == 32); + CHECK(scaled.size().height == 32); + CHECK(scaled.format() == surf.format()); + } + + TEST_CASE("Surface transformations - Flip") { + laya::context ctx{laya::subsystem::video}; + auto surf = create_test_surface({16, 16}); + + auto flipped_h = surf.flip(flip_mode::horizontal); + auto flipped_v = surf.flip(flip_mode::vertical); + auto flipped_none = surf.flip(flip_mode::none); + + // All should have same size and format + CHECK(flipped_h.size().width == surf.size().width); + CHECK(flipped_h.size().height == surf.size().height); + CHECK(flipped_v.size().width == surf.size().width); + CHECK(flipped_v.size().height == surf.size().height); + CHECK(flipped_none.size().width == surf.size().width); + CHECK(flipped_none.size().height == surf.size().height); + + CHECK(flipped_h.format() == surf.format()); + CHECK(flipped_v.format() == surf.format()); + CHECK(flipped_none.format() == surf.format()); + } + + TEST_CASE("Surface locking - Basic lock guard usage") { + laya::context ctx{laya::subsystem::video}; + auto surf = create_test_surface({8, 8}); + + { + auto lock = surf.lock(); + CHECK(lock.pixels() != nullptr); + CHECK(lock.pitch() > 0); + } + // Lock should be automatically released + } + + TEST_CASE("Surface locking - Lock guard move semantics") { + laya::context ctx{laya::subsystem::video}; + auto surf = create_test_surface({8, 8}); + + auto lock1 = surf.lock(); + void* original_pixels = lock1.pixels(); + + auto lock2 = std::move(lock1); + CHECK(lock2.pixels() == original_pixels); + // lock1 should be empty after move + } + + TEST_CASE("Surface metadata") { + laya::context ctx{laya::subsystem::video}; + auto surf = create_test_surface({42, 24}, pixel_format::rgb24); + + CHECK(surf.size().width == 42); + CHECK(surf.size().height == 24); + CHECK(surf.format() == pixel_format::rgb24); + CHECK_FALSE(surf.must_lock()); // SDL3 typically doesn't require locking + } +}