devkitPro / citro3d

Homebrew PICA200 GPU wrapper library for Nintendo 3DS
zlib License
244 stars 34 forks source link

Softlock when nothing is drawn #35

Open TurtleP opened 7 years ago

TurtleP commented 7 years ago

Specifically, if I have Citro render the top screen render target, but I don't draw anything, it will go through two cycles before soft locking. Removing this rendertarget stuff in aptMainLoop fixes this (which broke my FPS timer :P)

Emulation on Citra

Apparently the solution is to fool the render targets by applying a completely transparent texture at all times--something I'd rather not do. I've tested both builds on my 3DS and they behave as shown on Citra.

fincs commented 7 years ago

If you don't draw anything to a target, why do you bind it?

TurtleP commented 7 years ago

Well I'm unsure how to check if the end user will be drawing anything immediately. They could be writing their draw function into the script to set up for later. Add other events like keypressing and it doesn't work because of the softlock. If there's a way to fix this issue that I'm unaware of (if it's on my end), please let me know.

fincs commented 7 years ago

Why don't you let the end users bind the targets themselves?

TurtleP commented 7 years ago

The reason is because it's meant to be a game framework (i.e. Love Potion), but I'm rewriting it to use Citro rather than sf2dlib. The end users shouldn't need to know how to bind a rendertarget on the Lua side, that's just ugly and makes no sense. /shrug

fincs commented 7 years ago

It does make sense. Allowing the end user to control rendertargets allows for fun stuff to happen, such as caching a piece of drawing to a texture and using that multiple times when drawing to a screen. IIRC even sf2d did the same thing.

TurtleP commented 7 years ago

Right, I know. The issue, however, is that this is basically Love2D for 3DS. I'm following how it's done through the official code and tweaking where needed (such as font loading and filesystem access). The code looks like this on the Lua script:

function love.load()
    print("Press 'start' to quit")

    --print("Loading font!")
    timer = 1
end

function love.update(dt)
    timer = timer - dt
    if timer < 0 then
        print("FPS: " .. love.timer.getFPS())
        timer = 1
    end
end

function love.draw()    
    love.graphics.print("Adrian.. FACE REVEAL WEN?!?!?!", 120, 120)
end

function love.keypressed(key)
    if key == "start" then
        love.quit()
    end
end

The way it works is all I care about is knowing I can draw whenever. It doesn't need to be immediate. If I throw the program on the 3DS or Citra it should run without needing to draw something or having people familiar (or new) with this framework require knowledge about behind-the-scenes stuff.

fincs commented 7 years ago

You could try lazily starting the frame. Invoke love.draw(), and as soon as that method tries to use any drawing commands, start the frame. If after invoking said method a frame has been started, end it. Otherwise use C3D_FrameSync() to synchronize with the screens & keep it "60fps". You could also use the same idea to throw an error if the user script tries to use drawing commands outside love.draw().

TurtleP commented 7 years ago

Where would I FrameSync()? I thought Citro does that already. I'm also not sure if I can really 'detect' if the user tries to invoke a drawing command. I'll take a poke at it I guess.

This is my rendering code in C++

void Graphics::Render(gfxScreen_t screen)
{
    resetPool();

    renderScreen = screen;

    C3D_FrameBegin(C3D_FRAME_SYNCDRAW); //SYNC_DRAW

    switch(screen)
    {
        case GFX_TOP:
            this->StartTarget(topTarget);
            break;
        case GFX_BOTTOM:
            this->StartTarget(bottomTarget);
            break;
    }
}

void Graphics::StartTarget(CRenderTarget * target)
{
    if (target->GetTarget() == nullptr)
        return;

    target->Clear(graphicsGetBackgroundColor());

    C3D_FrameDrawOn(target->GetTarget());

    C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, projection_desc, target->GetProjection());
}

void Graphics::SwapBuffers()
{
    C3D_FrameEnd(0);
}

All of these methods do get called appropriately.

fincs commented 7 years ago

citro3d does that automatically, but only if you actually use C3D_FrameBegin/C3D_FrameEnd. Otherwise you're going to have to sync manually. Where do you invoke love.draw()?

TurtleP commented 7 years ago

I invoke it during the aptMainLoop:

while (aptMainLoop())
    {
        if (LOVE_QUIT)
            break;

        if (!LUA_ERROR)
        {
            ....

            if(luaL_dostring(L, "if love.update then love.update(love.timer.getDelta()) end"))
                console->ThrowError(L);

            ....

            love::Graphics::Instance()->Render(GFX_TOP);

            if (luaL_dostring(L, "if love.draw then love.draw() end"))
                console->ThrowError(L);

            /*love::Graphics::Instance()->Render(GFX_BOTTOM);

            if (luaL_dostring(L, "if love.draw then love.draw() end"))
                console->ThrowError(L);
            */
            love::Graphics::Instance()->SwapBuffers();

            love::Timer::Instance()->Tick();
        }
    }

Sorry for the spacing :P

fincs commented 7 years ago

Ok so you basically need to lazily call love::Graphics::Instance()->Render(GFX_TOP); the first time love.draw issues a drawing command (per frame) - for this you'll probably need a bool flag. SwapBuffers() (which I imagine does the FrameEnd call) should detect whether the frame has actually started - if it has, then end it; if it hasn't, use FrameSync.

TurtleP commented 7 years ago

I think I have the right way to do it, but my issue is most likely detecting if the frame really started. The render for GFX_TOP happens before love.draw executes. This allows for Citro to render images, primitive shapes, etc. Then it ends the frame. I don't have a way to definitely check if the user put in some rendering code.

fincs commented 7 years ago

No. I meant deferring the Render() call until love.draw actually calls drawing commands. You ought to have those drawing commands implemented in C, right? That's where you need to detect if Render() needs to be called.

(Btw, if it's not clear enough: Using the renderqueue frame commands without actually rendering anything is considered API misuse.)

TurtleP commented 7 years ago

Yeah the draw commands are in C. If I have to flag a Boolean in each one it's kind of hacky, tbh. I'll keep trying I guess. I was hoping there'd be a better fix.

Clownacy commented 7 years ago

I just had this same problem with my homebrew game, and I think I have something that works.

Before noticing this issue was here, I was looking through the commit log to see if I could find the cause. Along the way, I stumbled upon this commit.

C3D_RenderTargetSetClear is still supported, however it's unofficially
deprecated (it's performed after drawing/transferring instead of before
drawing, which is pretty counter-intuitive)

So, just to keep my code clean, I removed all uses of C3D_RenderTargetSetClear, and instead did it directly as part of my render function:

void Backend_Graphics_DrawFrame(void)
{
    aptMainLoop();      // Do this once per frame so sleeping doesn't crash
    C3D_FrameBegin(C3D_FRAME_SYNCDRAW);

        C3D_FrameDrawOn(render_target_left);
        drawing_right_eye = false;

        C3D_FrameBufClear(&render_target_left->frameBuf, C3D_CLEAR_ALL, CLEAR_COLOR, 0);    // Added line

        DrawObjects();

        C3D_TexBind(0, level_texture);
        DrawLevel();

        if (osGet3DSliderState())
        {
            C3D_FrameDrawOn(render_target_right);
            drawing_right_eye = true;

            C3D_FrameBufClear(&render_target_right->frameBuf, C3D_CLEAR_ALL, CLEAR_COLOR, 0);   // Added line

            DrawObjects();

            C3D_TexBind(0, level_texture);
            DrawLevel();
        }

    C3D_FrameEnd(0);
}

To my surprise, the crashes stopped happening.

fincs commented 7 years ago

Might as well add that calling C3D_FrameSplit() is necessary before doing any kind of external operation (clear, transfer, etc) to a rendertarget's framebuffer if (and only if) you have previously drawn anything to it in the current frame. In the code posted above this call isn't necessary since the framebuffer is cleared before it is being drawn to. An alternative way to clear a rendertarget without using C3D_RenderTargetSetClear is just to disable depthtest and draw a plain ol' colored quad over the entire buffer (which is what some games as well as new-hbmenu actually do).

TurtleP commented 7 years ago

So just basically clear the FrameBuffer rather than SetClear?

endrift commented 7 years ago

I'm being bitten by this right now too, FYI. Not all code is as simple and straightforward as you assume and telling someone "well just refactor it to make it more straightforward" is not exactly a solution.

TurtleP commented 7 years ago

I agree with @endrift since there shouldn't be a reason it softlocks using this function if nothing is there. Regardless, just using FrameBufClear works and doesn't cause a soft lock before drawing, but the color format to clear is different than expected.

Swiftloke commented 6 years ago

Just experienced this issue for myself. Render a frame, draw nothing, GPU crash (no crash handler, just hang). The context was me trying to clear the framebuffer to prevent issues with APT_DoApplicationJump (In which it gets weird and renders your last frame but... without the last thing you drew while jumping). Simply drawing a black rectangle does what I need (clearing the framebuffer would work too), but just keep in mind that more people are going to run in to this.

namkazt commented 6 years ago

i stuck on this thing few hours don't know why my hb suddenly stuck.