Skip to content

Implement Texture Scrolling Interpolation#969

Open
MegaMech wants to merge 28 commits intoKenix3:mainfrom
MegaMech:scrolltextureinterp
Open

Implement Texture Scrolling Interpolation#969
MegaMech wants to merge 28 commits intoKenix3:mainfrom
MegaMech:scrolltextureinterp

Conversation

@MegaMech
Copy link
Copy Markdown
Contributor

@MegaMech MegaMech commented Dec 22, 2025

First of all... Actually, hold on a second.

Merry Christmas! I hope everyone is hanging out with their family, and safe travels to everyone travelling during this time. Stay safe, stay warm (It's about to be -50 celcius where I'm from that's -58 in freedom units). And I hope you're playing a plethora of Christmas carols to annoy every family member who has been sick of hearing them play in the grocery store since September. Okay, with that out of the way.

First of all, I've had very little sleep. I'm tired, I'm emotional, I just want to go home. And as such, I'm going to need a little bit of grace on my next point (...and all the other ones as well).
Second of all, 2Ship can go stick their heads in a snowbank*.
Third of all, this is still wip and it may not quite work yet please provide as helpful suggestions as possible as we work together to find a solution that works for everyone. However, please refrain from statements such as I don't like this because 2ship will have to do more work or 2ship already implemented tex scrolling or I don't like this PR because I'm only getting coal and socks for Christmas because these are not very helpful statements, and why should I care anyway? You probably deserved the coal. Socks on the other hand, oooh, wouldn't wish that on my worst enemy, but still, sucks to suck!

I intend to do the following:

  • Implement the gbi.h commands 'properly' and not half [censored]
  • Implement the feature in a way that works for everyone, and not just myself *cough* *cough* 2ship *cough* *cough* *cough* *dies* *you bastards* Was I supposed to be dead? My bad
  • Implement the rsp and rdp versions of the gbi command so it works with static DLists (if you even know what that is).
  • Communicate and interact with the other ports so that we have a thriving community and attempt to provide at least basic technical advice to others where possible (Or at least are acquainted with each other).
  • Implement the interpolation on the (and this point is extremely critical) --> *INTERPRETER SIDE* <-- of the code-base where it belongs
    • Say it aloud with me on the INTERPRETER SIDE WHERE IT BELONGS, (For those who actually said it aloud, thank you. For those who didn't, please see my second point at the top of the PR)).
  • Investigate if the interpolation in the original 'method' (I use the word 'method' pretty loosely here), even works as well as may be desirable (using really shitty computer, so not sure).
    • If concludes the interpolation could be improved, try to see what could be done on that front
    • If concludes it's fine. Then maybe consider crediting the original author in this PR for their work (Ha, fat chance at that!)
  • Make sure everyone benefits from this PR
  • Deeply contemplate my mental status (I'm fine, just dramatically under paid).

*Please do not actually go stick your head in a snowbank. (I've been informed not to say this, but f it) No matter how much I wish you would

Todo

  • Possibly better place for mFrameCount (it's a clock)
  • Make GBI commands smaller
  • Make GBI macro

Possible Improvements

  • Currently stuck with matrix-based interpolations.
    • The texture scrolling could be interpolated more times but not currently possible because this is done in GfxSpTri1
  • Delta time
    • Couldn't get it fully unlinked from framerate
    • Might need more render frames to have any difference from frameCount
  • Replace G_SETTILESIZE with G_SCROLL_TEXTURE
    • Would need a way to plug in stepX/stepY

Port-side Implementation Instructions

Add this line beside mInterpolationIndex

interpreter->mInterpolationCount = mtx_replacements.size();
image Any ports using frameCounter like so: ```cpp gDPScrollTexture(gfx, 0, (this->frameCounter * -15) & 0xFF, 32, 64, 0, -15)); ``` Will need to remove frameCounter. This is done in lus now. The updated version would be `gDPScrollTexture(gfx, 0, 0, 0, 32, 64, 0, -15)`

Normal DL Example

Replace gDPSetTileSize with:

// Steps by Y 10 every frame
gDPScrollTexture(gfx++, tile, uls, ult, lrs, lrt, 0, 10);
// Using simpler terms:
gDPScrollTexture(gfx++, tile, x, y, width, height, 0, 10);

Static DL Example

Run this once at the start of your level. Be sure to that the life time of writableDList outlasts the level and draw phase otherwise it will crash if it becomes null

/**
 * This function finds a G_SETTILESIZE command and hooks it.
 * Then writes the interpolation command into the provided gfx array
 * The game continues as normal after leaving the writableDList
 *
 * @arg gfxAsset the model needing interpolated scrolling textures
 * @arg writableDList - Provide Gfx myGfx[3]. This memory MUST stay alive for the lifetime of the object
 */

void scroll_texture_interpolated(Gfx* writableDList, const char* gfxAsset, s32 stepX, s32 stepY) {
    int8_t opcode = 0;
    if ((NULL == writableDList) || (NULL == gfxAsset)) {
        return;
    }

    Gfx* gfx = (Gfx*) gfxAsset;
    if (GameEngine_OTRSigCheck(gfx)) {
        gfx = (Gfx*) ResourceGetDataByName(gfx);
    }

    while ((opcode = GFX_GET_OPCODE(gfx->words.w0) >> 24) != G_ENDDL) {
        if (opcode == (int8_t)G_SETTILESIZE) {
            // Get values from the old tile size command
            int32_t tile = _SHIFTR(gfx->words.w1, 24, 12);
            int32_t uls  = _SHIFTR(gfx->words.w0, 12, 12);
            int32_t ult  = _SHIFTR(gfx->words.w0, 0,  12);
            int32_t lrs  = _SHIFTR(gfx->words.w1, 12, 12);
            int32_t lrt  = _SHIFTR(gfx->words.w1, 0,  12);
            
            // Write over old command to point to the new writeableDList
            __gSPDisplayList(gfx, &writableDList[0]);

            // Write DL data into writableDList. gDPScrollTexture takes up two Gfx
            gDPScrollTexture(&writableDList[0], tile, uls, ult, lrs, lrt, stepX, stepY);
            gSPEndDisplayList(&writableDList[2]);
            break;
        }
        gfx++;
    }
}

Modify Scroll After Created Static DL Example

Level::Tick() {
    D_802B87CC = random_int(300) / 40;
    if (D_802B87C8 < 0) {
        D_802B87C8 = random_int(300) / 40;
    } else {
        D_802B87C8 = -(random_int(300) / 40);
    }
    //                                stepX                                         stepY
    scroll3[0].words.w1 = (SHIFTL(D_802B87C8, 32, 32)) | SHIFTL(D_802B87CC, 0, 32));
}

Possible Delta Time Solution

This solution didn't work because it locked the scroll speed into the framerate. Whereas this was mostly solved, it was still a tiny bit effect and didn't have a noticeable improvement over frameCount. It's also likely that the deltaTime function used by SpaghettiKart needs to be ported as it still uses the old macro for OS Time

If this was to be re-looked at, suggest waiting for Kenix tick/draw component PR and then having a deltaTime calculation baked into lus

bool gfx_set_tile_size_interp_delta_time_handler_rdp(F3DGfx** cmd0) {
    F3DGfx* cmd = *cmd0;
    Interpreter* gfx = mInstance.lock().get();
    //float lrs, lrt;
    float incX, incY;
    float deltaTime = Ship::Context::GetInstance()->GetWindow()->mDeltaTime;
    uint32_t tile = C0(0, 12);
    
    float stepX = (int32_t) ((*cmd0)->words.w1 >> 32);
    float stepY = (int32_t) (*cmd0)->words.w1;

    ++(*cmd0);

    float uls = *reinterpret_cast<float*>((((u8*)&(*cmd0)->words.w0)) + sizeof(float));
    float ult = *reinterpret_cast<float*>(&(*cmd0)->words.w0);
    float lrs = *reinterpret_cast<float*>((((u8*)&(*cmd0)->words.w1)) + sizeof(float));
    float lrt = *reinterpret_cast<float*>(&(*cmd0)->words.w1);
    // printf("rec uls %d ult %d lrs %d lrt %d\n", origin_uls, origin_ult, origin_lrs, origin_lrt);

    // Clamp deltaTime to a reasonable range to avoid drastic changes
    if (deltaTime > 0.1f) {
        deltaTime = 0.1f;  // Cap deltaTime to prevent excessive scrolling speed
    }

    /** Calculate Interpolation **/

    deltaTime /= gfx->mInterpolationCount;
    stepX /= gfx->mInterpolationCount;
    stepY /= gfx->mInterpolationCount;
    //deltaTime *= gfx->mInterpolationIndex;

    if (gfx->mInterpolationCount == 1) {
        deltaTime *= gfx->mInterpolationIndex + 1;
        stepX *= gfx->mInterpolationIndex + 1;
        stepY *= gfx->mInterpolationIndex + 1;
    } else {
        deltaTime *= gfx->mInterpolationIndex;
        stepX *= gfx->mInterpolationIndex;
        stepY *= gfx->mInterpolationIndex;
    }


    stepX *= deltaTime;
    stepY *= deltaTime;

    incX = (float)stepX;
    incY = (float)stepY;

    // Calculate the interpolation for the current frame
    uls += incX;// * gfx->mInterpolationIndex;
    ult += incY;// * gfx->mInterpolationIndex;

    lrs += incX;// * gfx->mInterpolationIndex;
    lrt += incY;// * gfx->mInterpolationIndex;

    // Clamp
    uls = std::fmod(uls, 2048);
    ult = std::fmod(ult, 2048);
    lrs = std::fmod(lrs, 2048);
    lrt = std::fmod(lrt, 2048);

    // Apply values to texture
    gfx->mRdp->texture_tile[tile].uls = uls;
    gfx->mRdp->texture_tile[tile].ult = ult;
    gfx->mRdp->texture_tile[tile].lrs = lrs;
    gfx->mRdp->texture_tile[tile].lrt = lrt;

    // Save the new values back into the gfx
    (*cmd0)->words.w0 = (uintptr_t) (*(uint32_t*)&uls << 32 | *(uint32_t*)&ult);
    (*cmd0)->words.w1 = (uintptr_t) (*(uint32_t*)&lrs << 32 | *(uint32_t*)&lrt);
    printf("applied uls %f ult %f lrs %f lrt %f delta %f\n", uls, ult, lrs, lrt, deltaTime);

    return false;
}

Troubleshooting

Note that SETTILESIZE command will over-write the values and likely reset the scrolling texture.
It's recommended to replace the tile size command with the texture scroll command.

@MegaMech MegaMech changed the title [wip] Implement Interpolate Texture Scrolling Implement Interpolate Texture Scrolling Dec 23, 2025
@MegaMech MegaMech changed the title Implement Interpolate Texture Scrolling Implement Texture Scrolling Interpolation Dec 27, 2025
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Clang-Tidy found issue(s) with the introduced code (1/1)

int32_t GetMouseCaptureScancode();
void SetFullscreenScancode(int32_t scancode);
void SetMouseCaptureScancode(int32_t scancode);
uint64_t mFrameCount;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ readability-identifier-naming ⚠️
invalid case style for public member mFrameCount

Suggested change
uint64_t mFrameCount;
uint64_t MFrameCount;

@MegaMech
Copy link
Copy Markdown
Contributor Author

This version seems to be a tad smoother since its based off of the actual FPS rate instead of interpolation rate. However, it requires upping your step value by ~1000

bool gfx_scroll_texture_handler_rdp(F3DGfx** cmd0) {
    F3DGfx* cmd = *cmd0;
    Interpreter* gfx = mInstance.lock().get();
    uint64_t frameCount = Ship::Context::GetInstance()->GetWindow()->mFrameCount;
    uint32_t tile = C0(0, 12);
    float incX, incY;

    int32_t stepX = (int32_t) C1(32, 32);
    int32_t stepY = (int32_t) C1( 0, 32);

    ++(*cmd0);

    float uls = *reinterpret_cast<float*>((((u8*)&(*cmd0)->words.w0)) + sizeof(float));
    float ult = *reinterpret_cast<float*>(&(*cmd0)->words.w0);
    float lrs = *reinterpret_cast<float*>((((u8*)&(*cmd0)->words.w1)) + sizeof(float));
    float lrt = *reinterpret_cast<float*>(&(*cmd0)->words.w1);

    incX = (float)stepX / (float)(ImGui::GetIO().Framerate);
    incY = (float)stepY / (float)(ImGui::GetIO().Framerate);
    //printf("incX %f incY %f fps %d fpsIndex %d\n", incX, incY, gfx->mTargetFps, gfx->mFpsIndex);

    uls += incX;
    ult += incY;
    lrs += incX;
    lrt += incY;

    uls = std::fmod(uls, 2048);
    ult = std::fmod(ult, 2048);
    lrs = std::fmod(lrs, 2048);
    lrt = std::fmod(lrt, 2048);

    // Apply values to texture
    gfx->mRdp->texture_tile[tile].uls = uls;
    gfx->mRdp->texture_tile[tile].ult = ult;
    gfx->mRdp->texture_tile[tile].lrs = lrs;
    gfx->mRdp->texture_tile[tile].lrt = lrt;

    // This isn't strictly necessary, but a port can
    // overwrite the tile size command making this necessary
    gfx->mRdp->textures_changed[0] = true;
    gfx->mRdp->textures_changed[1] = true;
    //printf("Scroll: uls %f ult %f lrs %f lrt %f incX %f incY %f fps %d interpCount %d\n", uls, ult, lrs, lrt, incX, incY, gfx->mTargetFps, gfx->mInterpolationCount);
    (*cmd0)->words.w0 = ((uintptr_t) *(uint32_t*)&uls << 32 | *(uint32_t*)&ult);
    (*cmd0)->words.w1 = ((uintptr_t)*(uint32_t*)&lrs << 32 | *(uint32_t*)&lrt);
    return false;
}

Copy link
Copy Markdown
Collaborator

@briaguya0 briaguya0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my main concerns here are the breaking changes. i know the PR description includes some good examples of how to use this, but it'd be good to have more of a "migration guide" (if you are currently doing X, when this PR lands you should do Y)

the most scary breaking change is the opcode reuse, i really want to hear from people using G_SETTILESIZE_INTERP because with this PR that'll be interpreted as G_SCROLL_TEXTURE which can wreak havoc

Comment on lines +518 to +519
uint32_t mInterpolationIndex;
uint32_t mInterpolationCount;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to verify no ports are relying on being able to pass in negative values here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interpolation index is generally a value from 0-16

constexpr int8_t OTR_G_LOAD_SHADER = OPCODE(0x43);
constexpr int8_t RDP_G_SETTILESIZE_INTERP = OPCODE(0x44);
constexpr int8_t RDP_G_SETTARGETINTERPINDEX = OPCODE(0x45);
constexpr int8_t RDP_G_SCROLL_TEXTURE = OPCODE(0x44);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is very much a breaking change. i'd want to either see a migration guide for people using the previous opcode defs or for this to be handled in a non-breaking way

#define G_LOAD_SHADER 0x43
#define G_SETTILESIZE_INTERP 0x44
#define G_SETTARGETINTERPINDEX 0x45
#define G_SCROLL_TEXTURE 0x44
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines -3216 to +3235
#define gDPSetInterpolation(pkt, index) \
_DW({ \
Gfx* _g = (Gfx*)(pkt); \
\
_g->words.w0 = G_SETTARGETINTERPINDEX << 24; \
_g->words.w1 = index; \
#define gDPScrollTexture(pkt, t, uls, ult, lrs, lrt, stepX, stepY) \
_DW({ \
Gfx* _g = (Gfx*)(pkt); \
if (pkt) \
; \
_g->words.w0 = (_SHIFTL(G_SCROLL_TEXTURE, 24, 8) | _SHIFTL(tile, 0, 12)); \
_g->words.w1 = (_SHIFTL(stepX, 32, 32) | _SHIFTL(stepY, 0, 32)); \
_g++; \
_g->words.w0 = (_SHIFTL(uls, 32, 32) | _SHIFTL(ult, 0, 32)); \
_g->words.w1 = (_SHIFTL(lrs, 32, 32) | _SHIFTL(lrt, 0, 32)); \
})

#define __gDPSetTileSizeInterp(pkt, t, uls, ult, lrs, lrt) \
gDPLoadTileGeneric(pkt, G_SETTILESIZE_INTERP, t, uls, ult, lrs, lrt)
#define gsDPScrollTexture(t, uls, ult, lrs, lrt, stepX, stepY) \
{ \
(_SHIFTL(G_SCROLL_TEXTURE, 24, 8) | _SHIFTL(tile, 0, 12)), \
(_SHIFTL(stepX, 32, 32) | _SHIFTL(stepY, 0, 32)), \
}, \
{ \
(_SHIFTL(uls, 32, 32) | _SHIFTL(ult, 0, 32)), (_SHIFTL(lrs, 32, 32) | _SHIFTL(lrt, 0, 32)), \
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. breaking changes (same comment as https://github.com/Kenix3/libultraship/pull/969/changes#r2678862060)
  2. there are some hardcoded magic numbers in here which makes me think this is removing control/flexibility from ports.

int32_t GetMouseCaptureScancode();
void SetFullscreenScancode(int32_t scancode);
void SetMouseCaptureScancode(int32_t scancode);
uint64_t mFrameCount;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tidy is right to be mad about this. it shouldn't be public. make it private and add a public getter and from what i can tell you should be able to use a protected setter

}

void Fast3dWindow::HandleEvents() {
mFrameCount += 1;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this in HandleEvents?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MegaMech
Copy link
Copy Markdown
Contributor Author

image The old command would no longer be supported and games using it would need to update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants