Troubleshooting & FAQ¶
Common issues when integrating WhatsCanvas, and how to fix them. Most stem from the library's core contract: it renders; it does not own your window, GL context, or file I/O.
Rendering¶
My image draws as a solid black rectangle¶
drawImage tints the image by the paint's color, and the default Paint
color is black — so a default paint renders the image black. Use white to
draw the image untouched:
wsc::Paint tint;
tint.setColor(wsc::Color::WHITE); // original image, no tint
canvas->drawImage(image, x, y, tint);
Nothing renders (or a crash) with the OpenGL backend¶
The OpenGL backend never creates a window or GL context — your app (or GLFW/SDL/Qt) does. The required order is:
- Create a GL context and make it current.
wsc::Canvas::loadOpenGL(loader)— hand over your platform's proc loader.canvas.setSize(w, h)thencanvas.initializeContext().canvas.beginFrame()→ draw →canvas.endFrame()with the context current.
If you skip step 1–2 or draw without a current context, GL calls fail. The
software and Vulkan backends (Canvas::create(Backend::Software, ...) /
Canvas::create(Backend::Vulkan, ...)) need none of this — call beginFrame()
to initialize them lazily before drawing.
Colors look washed out / semi-transparent blends look wrong¶
By default WhatsCanvas blends in straight sRGB space. For physically-correct (linear-space) alpha blending, enable gamma correction:
The software backend mirrors the GL behaviour exactly, so output matches across backends.
My exported PNG is upside-down¶
readPixelsRGBA already returns top-left-origin rows. If your image writer
also flips vertically (e.g. stbi_flip_vertically_on_write(1)), you get a
double flip. Don't flip again.
readPixelsRGBA returns all zeros (black / empty image)¶
On the Software backend's normal framebuffer path, a common cause is a
second endFrame() after you already ended the frame. endFrame()
consumes the recorded commands, so the second call has nothing to draw while
Software clears that framebuffer to transparent. Vulkan, OpenGL, and a
render-target canvas created with OffscreenTexture() retain their existing
target contents on an empty submission, but the extra call is still a lifecycle
error.
The correct offscreen sequence is:
canvas->beginFrame(); // initializes lazily; resets queued frame state
canvas->drawRect(/* ... */, paint);
canvas->endFrame(); // <- exactly once, right before reading
canvas->readPixelsRGBA(pixels); // or savePixelsPPM("out.ppm")
Call endFrame() exactly once per frame, and do not call beginFrame()
after drawing (it discards queued commands before submission). Also confirm you
actually drew inside the canvas bounds with a non-transparent paint color.
OpenGL does not implicitly clear a host-owned framebuffer; clear it explicitly
when a fresh background is required.
Text¶
Text doesn't appear¶
Check, in order:
- Paint color — a default (black) paint on a dark background is invisible.
- Font — for deterministic output, register a font and set the family:
- Position —
drawText(text, x, y, paint)places text at a baseline; make sure it is within the canvas.
Glyphs are missing or fall back to boxes¶
Register a fallback chain so mixed scripts resolve to the right face:
wsc::FontFallbackChain chain("Inter");
chain.addFallbackFamily("Noto Sans CJK");
canvas->setFontFallbackChain(chain);
FreeType and HarfBuzz are enabled by default
(-DWHATSCANVAS_ENABLE_FREETYPE_RASTERIZER=ON,
-DWHATSCANVAS_ENABLE_OPENTYPE_SHAPING=ON). When explicitly disabled or not
found, WhatsCanvas falls back to stb_truetype and simple shaping and reports
it in the text-backend diagnostics. The standalone Software target always uses
that built-in fallback stack.
Backends¶
How do I know if Vulkan is available? / graceful fallback¶
Vulkan is opt-in at build time (-DWHATSCANVAS_ENABLE_VULKAN=ON + a Vulkan SDK)
and compiles into WhatsCanvas::OpenGL (there is no separate ::Vulkan
target). Probe at runtime and fall back:
using Backend = wsc::Canvas::Backend;
std::unique_ptr<wsc::Canvas> canvas =
wsc::Canvas::isBackendAvailable(Backend::Vulkan)
? wsc::Canvas::create(Backend::Vulkan, w, h)
: wsc::Canvas::create(Backend::Software, w, h);
The Vulkan backend renders off-screen by default. On Win32 it can also use
OutputTarget::ToWindow(...) + present(); for portable/headless usage, read
the result with readPixelsRGBA.
I want a binary that links no GPU libraries at all¶
Build the standalone software target:
cmake -S . -B build -DWHATSCANVAS_BUILD_OPENGL=OFF -DWHATSCANVAS_BUILD_SOFTWARE=ON
target_link_libraries(MyApp PRIVATE WhatsCanvas::Software)
Context loss on mobile (Android background)¶
On GL context loss, release and re-initialize:
canvas.releaseResources();
// ... after the platform re-creates the context and makes it current ...
canvas.initializeContext();
present() returns false / nothing shows in my window¶
On-screen presentation is platform-dependent. Supported today: software (Windows GDI + Linux X11), OpenGL (WGL; GLX on Linux), and Vulkan (Windows). Checklist:
- Call
setOutputTarget(OutputTarget::ToWindow(surface))and check its return value — it isfalsewhen presentation is unsupported for the current backend/platform or the surface has no window handle. Fall back accordingly (e.g.glfwSwapBuffersfor GL, or off-screen +readPixelsRGBA). - Initialize the backend before configuring a window or external target:
initializeContext()is required for Vulkan and for the OpenGL renderer afterloadOpenGL(...); it is harmless but optional for Software. - Fill the surface correctly:
platform = NativeSurface::Platform::Win32andwindow = <HWND>(e.g.glfwGetWin32Window(window)). - Create the window without a GL context for the software or Vulkan backend
(
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API)); for the OpenGL backend, make the GL context current and callCanvas::loadOpenGLfirst. - Present each frame after
endFrame():beginFrame → draw → endFrame → present. - If you include
<windows.h>(or a native GLFW header) in the same file, include thewsc/headers first and defineNOMINMAX, so themin/maxmacros do not break WhatsCanvas headers.
See the software_present, gl_present, and vulkan_canvas_present
examples for
working setups.
Build & packaging¶
find_package(WhatsCanvas ...) can't be found¶
- Build a package first (
build.bat --release --package --no-run) or download a release archive, then point CMake at it:cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/package. - Use the exact version:
find_package(WhatsCanvas 0.2.0 CONFIG REQUIRED).
Do consumers need GLFW / GLAD / GLM?¶
No. GLFW is only for the in-repo example windows, GLAD is compiled into the
GL-family backend, and GLM is an internal math dependency. Consumers include and
link only WhatsCanvas::OpenGL (or ::Software / ::OpenGLES) and include/wsc/.
Windows: LNK2019 unresolved external symbol __std_min_element_f_ (or __std_max_element_f_)¶
This is an MSVC standard-library (STL) toolset-version mismatch when linking a
prebuilt binary. Newer MSVC toolsets dispatch std::min_element / max_element
(etc.) on trivial types to out-of-line, ABI-versioned SIMD helpers whose symbols
older STL runtimes do not provide, so a library compiled with a newer toolset
fails to link on an older Visual Studio.
WhatsCanvas builds its own binaries with _USE_STD_VECTOR_ALGORITHMS=0 (scalar
path), so the shipped libraries do not reference these version-specific
symbols and link against any VS 2022 STL. If you still hit this — e.g. building
WhatsCanvas yourself, or linking another prebuilt library — fix it by any of:
- Update Visual Studio 2022 so your toolset is at least as new as the one that built the binary (VS Installer -> Update), then rebuild.
- Build the offending library from source with your own toolset (identical STL on both sides).
- When building a library for redistribution, compile it with
-D_USE_STD_VECTOR_ALGORITHMS=0(MSVC) so its objects avoid the versioned helpers.
There is no way to "use a different STL" here: on MSVC the standard library is the MSVC STL. The mismatch is a general C++ binary-compatibility (ABI) issue, not specific to WhatsCanvas.
Diagnostics & logging¶
WhatsCanvas has a built-in logging facility (wsc/Log.h) that reports
recoverable problems and failures. By default only Warning and Error
messages are written to stderr.
See more detail while debugging¶
Lower the threshold to surface informational and debug messages:
Route logs into your own system¶
Install a handler to forward every message (level, category, text) wherever you want — a file, an in-game console, spdlog, etc.:
wsc::Log::setHandler([](const wsc::LogMessage &m) {
myLogger.log(m.level, m.category, m.message);
});
Pass nullptr to restore the default stderr sink. Use
wsc::Log::setLevel(wsc::LogLevel::Off) to silence all output.
Common messages¶
| Category | Meaning |
|---|---|
Image |
An image failed to decode or its texture could not be created. |
DrawValidation |
A draw call was skipped (empty vertices, bad dimensions, invalid resource). |
GLProgram / OpenGL |
Shader compile/link failure or a GL error was detected. |
RenderDeviceFactory |
No usable render backend was found. |
VulkanRenderDevice |
Vulkan device/instance setup failed, or Vulkan is not compiled in. |
Deprecation |
A deprecated API was called (emitted once per call site). |
Still stuck?¶
- Skim the Get Started guide and the runnable
tests/package_consumer. - Check the API Reference and API Stability.
- Open an issue on GitHub.