Thursday, December 31, 2020

Wrapping C API with C++ Part 2

In my last post I talked about adding RAII to a C API by wrapping it using guard objects. In this post I'm going to talk about dealing with an API that does not maintain its invariant. The API I'm using is fortunately (or unfortunately) the same SDL2 API from the last post, and the same SDL_Renderer management API. I found the same problem as discussed in this Stack Overflow question. The root of the problem are a pair of functions SDL_SetRenderTarget and SDL_GetRenderTarget. These are, it appears, intended to function similarly to SDL_RenderSetClipRect
and SDL_RenderGetClipRect. The Get version should return the same value as the Set version; but does not. The reason is in one of the answeres to the Stack Overflow question. The bug manifests in that, unless you save the Render Target yourself under the correct circumstances the program will not behave as intended.

There are at least two ways to solve this:

  1. I could modify my Renderer class to provide storage for the last Render Target set and modify RenderTargetGuard to work with that storage instead.
  2. I could use the C++ Standard Library container std::stack to implement a stack of past Render Targets.

I chose option 2. If my code was going to be creating and destroying many Renderers in the course of operation option 1 would have been better. But that is not the case; only a small number of Renderers are created at start up and used  throughout the program. Option 2 makes the Renderer class more complex but also lends itself to adding convenience functions to bring more of the functions of the SDL_Renderer API within the scope of idiomatic C++. Here is my new Renderer class (at the time this is written):

/**
* @class Renderer
* @brief Written as a workaround for an issue in the SDL2 Library.
* @details https://stackoverflow.com/questions/50415099/sdl-setrendertarget-doesnt-set-the-tartget
*/
class Renderer {
protected:
friend class RenderTargetGuard;

/**
* @brief A functor to destroy an SDL_Renderer
*/
class RendererDestroy {
public:
void operator()(SDL_Renderer *sdlRenderer) {
SDL_DestroyRenderer(sdlRenderer);
}
};

using RendererPtr = std::unique_ptr<SDL_Renderer, RendererDestroy>; ///< An SDL_Renderer unique pointer
RendererPtr mRenderer{}; ///< The Renderer.

std::stack<SDL_Texture*> mTargetTextureStack{}; ///< The stack of render targets

/**
* @brief Pop a render target off the stack. If there are none, set the default render target.
* @details The top of the stack is the current render target, unless the default is current in which
* case the stack will be empty. If the stack has 1 or 0 render targets on it, the renderer is set
* to the default target.
* @return The return status of SDL_SetRenderTarget
*/
[[nodiscard]] int popRenderTarget() {
if (mTargetTextureStack.empty())
return SDL_SetRenderTarget(mRenderer.get(), nullptr);
else {
mTargetTextureStack.pop();
if (mTargetTextureStack.empty())
return SDL_SetRenderTarget(mRenderer.get(), nullptr);
else
return SDL_SetRenderTarget(mRenderer.get(), mTargetTextureStack.top());
}
}

/**
* @brief Set a new render target, and push it onto the stack.
* @param texture The new render target.
* @return The return status of SDL_SetRenderTarget
*/
[[nodiscard]] int pushRenderTarget(sdl::Texture &texture) {
mTargetTextureStack.push(texture.get());
return SDL_SetRenderTarget(mRenderer.get(), texture.get());
}

/**
* @brief Set the render target to the default, and push it onto the stack.
* @return The return status of SDL_SetRenderTarget
*/
[[nodiscard]] int pushRenderTarget() {
mTargetTextureStack.push(nullptr);
return SDL_SetRenderTarget(mRenderer.get(), nullptr);
}

public:
/**
* Construct an empty renderer.
*/
Renderer() = default;

/**
* Construct a renderer associated with a Window.
* @param window The associated Window.
* @param index The index argument to SDL_CreateRenderer.
* @param flags The flags argument to SDL_CreateRenderer.
*/
Renderer(Window& window, int index, Uint32 flags);

/**
* @brief Move assignment operator.
* @param renderer The renderer to assign, this becomes empty after the assignment.
* @return A reference to this renderer.
*/
Renderer& operator=(Renderer&& renderer) = default;

/**
* @brief Test the renderer.
* @return false if the renderer is empty, true if it is valid.
*/
explicit operator bool () const noexcept { return mRenderer.operator bool(); }

/**
* @brief The the underlying SDL_Renderer* for use with the SDL2 API.
* @return An SDL_Renderer*
*/
[[nodiscard]] auto get() const { return mRenderer.get(); }

/**
* @brief Set the SDL_BlendMode on the renderer.
* @param blendMode
*/
void setDrawBlendMode(SDL_BlendMode blendMode) {
SDL_SetRenderDrawBlendMode(mRenderer.get(), blendMode);
}

/**
* @brief Create a Texture with the given size.
* @param size The Texture size.
* @return a new Texture object.
*/
Texture createTexture(SizeInt size);

/**
* @brief Copy source Texture to destination Texture and set the BlendMode on the destination Texture.
* @details The function uses RenderTargetGuard to temporarily set the render Target to the destination,
* calls SDL_RenderCopy to copy the texture, and sets the BlendMode on the destination texture to
* SDL_BLENDMODE_BLEND.
* @param source
* @param destination
*/
void copyFullTexture(sdl::Texture &source, sdl::Texture &destination);

/**
* @brief Calls SDL_RenderClear on the renderer.
* @return The return status from SDL_RenderClear.
*/
int renderClear() { return SDL_RenderClear(mRenderer.get()); }

/**
* @brief Calls SDL_RenderPresent on the renderer.
*/
void renderPresent() { SDL_RenderPresent(mRenderer.get()); }

/**
* @brief Calls SDL_RenderCopy to copy the source texture to the current render target.
* @details SDL_RenderCopy is called with nullptr for srcrect and dstrect.
* @param texture The texture to copy.
* @return The return status of SDL_RenderCopy.
*/
int renderCopy(Texture &texture) { return SDL_RenderCopy(mRenderer.get(), texture.get(), nullptr, nullptr); }

/**
* @brief Calls SDL_RenderFillRect after setting the RenderDrawColor to color.
* @details The existing RenderDrawColor is saved and restored.
* @param rectangle The rectangle to fill.
* @param color The fill color.
* @return The return status of SDL_RenderFillRect.
*/
int fillRect(RectangleInt rectangle, Color color);
};

And here is the new RenderTargetGuard class:

/**
* @class RenderTargetGuardException
* @brief Thrown by RenderTargetGuard on errors.
*/
class RenderTargetGuardException : public SdlRuntimeException {
public:
explicit RenderTargetGuardException(const std::string &what_arg) : SdlRuntimeException(what_arg) {}
};

/**
* @class RenderTargetGuard
* @brief Store the current render target replacing it with a new render target. When the object is
* destroyed (by going out of scope) the old render target is restored.
*/
class RenderTargetGuard {
protected:
Renderer &mRenderer;
bool popped{false};
int status{0};

public:
RenderTargetGuard() = delete;
RenderTargetGuard(const RenderTargetGuard&) = delete;

/**
* @brief Set the old render target back on the renderer when destroyed.
*/
~RenderTargetGuard() noexcept(false) {
if (!popped) {
status = mRenderer.popRenderTarget();
if (status)
throw RenderTargetGuardException(util::StringCompositor("Call to SDL_SetRenderTarget failed:",
SDL_GetError()));
}
}

/**
* @brief Test the status of the RenderTargetGuard.
* @details The status is good (true is returned) if the status value returned from the last
* operation on the Renderer object returned 0.
* @return True if the last operation succeeded.
*/
explicit operator bool () const noexcept { return status == 0; }

/**
* @brief Constructor
* @param renderer The renderer which render target will be managed.
* @param texture The texture which will become the new render target.
*/
RenderTargetGuard(Renderer &renderer, Texture &texture) : mRenderer(renderer) {
status = mRenderer.pushRenderTarget(texture);
}

/**
* @brief Clear the render target so rendering will be sent to the screen backing buffer.
*/
void clear() {
status = mRenderer.popRenderTarget();
popped = true;
}

/**
* @brief Set a new render target without pushing it on the stack.
* @details This may be used when a number of render target changes are needed in a context block.
* A RenderTargetGuard is created, calls to setRenderTarget are used to manipulate the render target.
* When the RenderTargetGuard goes out of scope the original render target is restored.
* @param texture
* @return
*/
int setRenderTarget(Texture &texture) {
status = SDL_SetRenderTarget(mRenderer.get(), texture.get());
return status;
}
};

No comments:

Post a Comment