Poor's Man Shaders

3 months ago 2
29 Jul 2025

Spoiler: it’s not shaders

I’m waiting for a universal solution that the SDL developers are working on — cross-platform, multi-API shaders, the SDL_shadercross. The idea is that you write shaders in a single language, and at runtime, they get compiled for the target GPU. Unfortunately, it’s a large and complex project, and it will take time before it becomes stable.

In the meantime, in my Carimbo engine, I was wondering if I could implement something similar to shaders — something that would allow Lua code to write arbitrary pixels into a buffer and stream that buffer into a texture.

So I created what I call a canvas, which is basically a texture the same size as the screen, rendered after certain elements.

canvas::canvas(std::shared_ptr<renderer> renderer) : _renderer(std::move(renderer)) { int32_t lw, lh; SDL_RendererLogicalPresentation mode; SDL_GetRenderLogicalPresentation(*_renderer, &lw, &lh, &mode); float_t sx, sy; SDL_GetRenderScale(*_renderer, &sx, &sy); const auto width = static_cast<int32_t>(std::lround(static_cast<float>(lw) / sx)); const auto height = static_cast<int32_t>(std::lround(static_cast<float>(lh) / sy)); SDL_Texture *texture = SDL_CreateTexture(*_renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height); if (!texture) [[unlikely]] { throw std::runtime_error(std::format("[SDL_CreateTexture] {}", SDL_GetError())); } SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); _framebuffer.reset(texture); } void canvas::set_pixels(const uint32_t* pixels) noexcept { const auto ptr = _framebuffer.get(); const auto pitch = static_cast<int>(static_cast<size_t>(ptr->w) * sizeof(uint32_t)); SDL_UpdateTexture(ptr, nullptr, pixels, pitch); }

The set_pixel function receives a pointer to a uint32_t buffer that exactly matches the texture size. This pointer is actually a Lua string, which I found to be the most performant way to transfer data between Lua and C++ without relying on preallocated buffers.

This way:

lua.new_usertype<graphics::canvas>( "Canvas", sol::no_constructor, "pixels", sol::property( [](const graphics::canvas&) { return nullptr; }, [](graphics::canvas& canvas, const char* data) { canvas.set_pixels(reinterpret_cast<const uint32_t*>(data)); } ) );

On Lua side:

function Effect:new(width, height) local w, h = width or 480, height or 270 local canvas = engine:canvas() local self = setmetatable({ canvas = canvas, w = w, h = h, }, Effect) return self end function Light:loop() self.canvas.pixels = rep(char(0, 0, 0, 220), self.w * self.h) end

Some effects I’ve created so far:

https://youtu.be/GUWTWRQuzxw

https://youtu.be/usJ9QM7V8BI

Read Entire Article