Skip to content

remap function included in the std library #1899

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
emkkla opened this issue Nov 5, 2024 · 9 comments · May be fixed by #1955
Open

remap function included in the std library #1899

emkkla opened this issue Nov 5, 2024 · 9 comments · May be fixed by #1955

Comments

@emkkla
Copy link

emkkla commented Nov 5, 2024

The need for a remap() function (also called efit() in VEX) is very common in shading, and it would be appreciated if the standard library included it natively.

So far, of course, we can remap vectors or floats by simply defining the function manually:

vector vectorRemap(vector input, vector minIn, vector maxIn, vector minOut, vector maxOut) {
    return minOut + (input - minIn) * (maxOut - minOut) / (maxIn - minIn);
}
float floatRemap(float input, float minIn, float maxIn, float minOut, float maxOut) {
    return minOut + (input - minIn) * (maxOut - minOut) / (maxIn - minIn);
}

But, having it part of the std would simplify code and ensure a proper implementation:

type remap(type input, type minIn, type maxIn, type minOut, type maxOut)
@lgritz
Copy link
Collaborator

lgritz commented Nov 10, 2024

That's a good idea, I will do so.

@lgritz lgritz self-assigned this Nov 10, 2024
@lgritz lgritz removed their assignment Mar 2, 2025
@lgritz
Copy link
Collaborator

lgritz commented Mar 2, 2025

If this helps anybody who wants to work on this, quite a bit before this issue was even filed, I had made the following note ot myself elsewhere:


We already have the standard function

    mix(a,b,x) = a*(1-x)+b*x;

the inverse also seems useful:

    unmix(a,b,x) = (x-a)/(b-a);

and with that ingredient, we also can easily make

    remap(a,b,c,d,x) = mix(c,d,unmix(a,b,x))

which remaps x's relative placement between a and b to the analogous proportionate position between c and d.

@etheory
Copy link
Contributor

etheory commented Mar 2, 2025

Random comment but mix is better implemented for performance reasons as:
mix(a,b,x)=a+(b-a)*x;

Which keeps everything in registers and avoids the need to load the 1.f from memory, which can make quite a difference.

Also, again, for performance reasons, the mix unmix version uses 7 ops whereas the version emklla suggests above uses 6, so whilst seemingly more verbose, I would expect the expanded version to execute faster, since the optimizer won't be able to remove the redundant op I think.

@lgritz
Copy link
Collaborator

lgritz commented Mar 2, 2025

We intentionally trade a little performance (maybe? see below) for numerical stability. The problem with a+(b-a)*x is that it looks good as "Real number mathematics", but when implemented with float approximations, it can be the case that a+(b-a) is not actually equal to b. The alternate expression a*(1-x) + b*x is 100% guaranteed to be exactly equal to a when x==0.0 and exactly equal to b when x==1.0. Getting the endpoints exactly right is important when doing interpolation.

I'm skeptical about performance claims here for two reasons: First, yes, looking at that one expression in complete isolation, that 1.0 needs to come from somewhere -- memory, if this is the only statement you are considering. But if it's near any other math that involves a 1.0, that value may already be in a register, so the cost can be amortized across multiple mix or other operations. Second, don't forget that we are doing really aggressive runtime optimization before lowering to LLVM IR. If any of a, b, or x can be identified as having known values at that point (which is common, and includes the case of any of them being non-interpolated parameters of the shader), the expression may get simplified or even optimized away entirely. (Also, turning any of those values into constants just puts us into the same pickle of having constant values that need to be loaded from somewhere.) So the idea of exactly what assembly the mix will turn into and what might be in registers is really squirrelly when you're talking about OSL source code and don't know the parameter values yet.

@etheory
Copy link
Contributor

etheory commented Mar 2, 2025

That's all good points, I don't disagree with any of it. And yes, subtractive cancellation is a real pain. But I've definitely experienced better performance in the real-world (where the end point didn't matter, as they do here, I agree), but certainly not in the context of a JIT'ed shader, where yes, absolutely, there is the global optimization step that's quite different to comparison to an inlined C++ function, and that 1 has a higher change of existing elsewhere to be re-used, especially if it ends up in cache.

But the mix unmix case still uses 1 more op, in theory, than the explicit remap implementation.

@lgritz
Copy link
Collaborator

lgritz commented Mar 2, 2025

Could be. I don't have a strong feeling about the specific implementation of remap. Mostly I had just jotted down in my notes that there's an elegant way to implement remap from a mix of an unmix.

@lgritz lgritz closed this as completed Mar 2, 2025
@lgritz lgritz reopened this Mar 2, 2025
@lgritz
Copy link
Collaborator

lgritz commented Mar 2, 2025

Sorry, I didn't mean to close and re-open this, just had the cursor in the wrong place at the moment I accidentally pushed on the trackpad.

@fpsunflower
Copy link
Contributor

This formulation avoids needing to load the 1.0f constant and gives the exact result with only two instructions (if you have a fused multiply add, which most modern instruction sets have).

@etheory
Copy link
Contributor

etheory commented Mar 2, 2025

This formulation avoids needing to load the 1.0f constant and gives the exact result with only two instructions (if you have a fused multiply add, which most modern instruction sets have).

Fascinating, thanks.

@Sparsh-N Sparsh-N linked a pull request Mar 9, 2025 that will close this issue
4 tasks
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 a pull request may close this issue.

4 participants