3
\$\begingroup\$

I am trying to rotate a vector made of two signed 16-bit coordinates (-32768 to 32767). Using a LUT for sin/cos, I can use the standard method for rotating a vector:

newX = x * cos(theta) - y * sin(theta);
newY = x * sin(theta) + y * cos(theta);

However, since this requires multiplying the coordinates by sin/cos (in my case, ranging from -127 to 127), large vectors can easily overflow in the middle of the process before being scaled back down. This means either the length of the vector or the accuracy of the sin/cos is severely limited, despite the original and resulting rotated vector being safely in range.

Is there another method for rotating integer vectors?

Edit: The numbers cannot be cast to floats or higher-bit types.

\$\endgroup\$
5
  • 3
    \$\begingroup\$ Mandatory notice, make sure that changed x wont affect y calculation (typical novice mistake). Rewrite it in a way of newx = x * cos(theta) - y * sin(theta); newy = x * sin(theta) + y * cos(theta); \$\endgroup\$
    – Kromster
    Commented Oct 9 at 9:56
  • 2
    \$\begingroup\$ I wonder whether rotation by shearing would be appropriate here. It's in some sense an "integer-native" way to do planar rotations, guaranteeing that every input vector gets mapped to a distinct output vector (they never round together in the process), and every integer pair within the interior of the rotated region is reached by some integer pair in the pre-image (there are never any gaps that open up from nearby inputs rounding in opposite directions). \$\endgroup\$
    – DMGregory
    Commented Oct 9 at 12:04
  • 3
    \$\begingroup\$ What are the resource limitations? Memory? CPU/ALU/DSP operations? There are several multiplication decomposition algorithms available to meet certain constrants(memory, intermediate storage, cpu cycles, precision...) \$\endgroup\$
    – agone
    Commented Oct 9 at 21:52
  • 4
    \$\begingroup\$ The numbers cannot be cast to floats or higher-bit types. - Why not? 32-bit temporaries don't cost extra memory bandwidth, just need wider registers (or more registers if you need to do a widening multiply, 16x16 -> 32-bit and take some of the high half. Or 8x16 -> 24-bit I guess if your trig functions are 8-bit LUT results.) Are you using 16-bit integer SIMD, like on x86 CPUs? If so, pmulhw (felixcloutier.com/x86/pmulhw) gives you the high half of a 16x16 signed multiply, pmullw gives you the low half. \$\endgroup\$ Commented Oct 10 at 1:59
  • 3
    \$\begingroup\$ Just about everything that does non-floating-point multiplication produces a double-sized result, from Babbage's Analytical Engine to modern desktop computers. \$\endgroup\$
    – Mark
    Commented Oct 10 at 2:57

6 Answers 6

4
\$\begingroup\$

Use 32-bit integers for the intermediate calculations.

They'll usually be divided down into the 16-bit range at the end. If they're not - for example, rotating \$(30000,30000)\$ by an eighth-turn should give \$(0,42426)\$ which doesn't fit into 16 bits - check it and produce an error at this point.

\$\endgroup\$
6
  • 1
    \$\begingroup\$ I don't know why you're using 16-bit numbers, so there might be some technical reason why you can't use 32-bit internally. But this is at least a starting point. If it doesn't work then there's other, more complicated approaches. \$\endgroup\$
    – Toph
    Commented Oct 9 at 7:46
  • 3
    \$\begingroup\$ I don't know author's constraints, but most obvious solution to me would be cast everything to float, calculate and cast result back to 16bits (with optional rounding). \$\endgroup\$
    – Somnium
    Commented Oct 9 at 15:25
  • 2
    \$\begingroup\$ ...and if you can't use 32 bits for some reason, you can do what bignum libraries do and implement one of the many multiplication algorithms, with base 2^8 or so. Probably Karatsuba is best with such small digit counts. \$\endgroup\$ Commented Oct 9 at 16:16
  • \$\begingroup\$ Unfortunately the OP has excluded both these options in their latest edit. :/ \$\endgroup\$
    – Graham
    Commented Oct 9 at 20:51
  • 1
    \$\begingroup\$ @Somnium: Any platform where 16 bits integers are a sensible choice for X/Y coordinates is also a platform where float is ridiculously expensive. \$\endgroup\$
    – MSalters
    Commented Oct 10 at 13:19
4
\$\begingroup\$

There are two issues to address here, each of which has a couple of solutions.

Size of vectors

Your first problem is how you do any processing at all without overflows. A vector (32767, 0) is clearly going to rotate nicely to anywhere. However a vector (32767, 32767) is going to become (49339, 0) when it's rotated 45 degrees, and that'll overflow. Even (-32768, 0) is going to overflow if you rotate it by 180 degrees.

Limit vector magnitude

You might solve this by having something limit the vectors entered so that their magnitude is small enough to fit. If data is being entered from a PC, you can use that to only give you valid data. Calculating a magnitude needs a square root though, which is going to be tricky on a constrained device.

Or only store 15-bit values ####8

If these checks are too much of a problem, you could just limit the range of values for vector entry to signed 15-bit. Processing can then take place in signed 16-bit arithmetic, knowing that the result cannot overflow. This would be my preferred solution, not just for simplicity, but because you have engineered out any possibility of failure.

Calculation of rotation

And then we come to how you do the rotation calculation without overflow. The sin/cos clearly is a fraction. I would strongly recommend having an extra bit in the sin/cos lookup value so that +1 can be exactly represented (e.g. +/-128 in a 9-bit value; or +/-64 if constrained to 8-bit). But from there, how to avoid overflow?

Use your processor's multiply instruction registers

On pretty much all processors with a multiply instruction, you have two registers in and two registers out. If you start with two 16-bit values, you get one 32-bit value held in two registers. If you're using +/-64 to represent +/-1, you'll need to divide the result by 64 - which conveniently just means discarding the lower 7 bits. With some simple bit-shifting and masking, it's easy to get a single word containing the result you want.

Break the multiply into lots of conditional adds

If you've got a sin/cos value of 64 (representing 1.0) then the input value passes through unchanged. Each lower bit after that represents a fraction of the input - 32 represents a half, 16 represents a quarter, and so on. You can add all these fractions together, knowing that the result can never overflow. Of course this takes a bunch of operations, but you're optimising for storage over speed. (You might also get some rounding errors in the lsbs, but again, this is prioritising storage over all else.)

But more generally...?

I'd have to wonder about where these limitations came from. If you're on an FPGA, it's unlikely you're so constrained by storage as to be worried by this. And if you're using a micro, it's rare that you'd be this limited these days.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Calculating a magnitude needs a square root though - You can compare magnitude-squared against limit-squared. Like in a Mandelbrot fractal iteration, you just check re^2 + im^2 >= 4 instead of square-rooting to check against 2. Square root is monotonic and smooth so this works. Even for runtime-variable limits, you can just square the limit to see if you need to clamp or not, although I guess you'd still need at least a division if you do need to normalize, to get a vector in the same direction with max magnitude. \$\endgroup\$ Commented Oct 10 at 2:07
  • \$\begingroup\$ But yeah, good suggestion to limit vector components to signed 15-bit, at least before rotation. So [16383, 16383] can rotate to [23169, 0] \$\endgroup\$ Commented Oct 10 at 2:40
  • \$\begingroup\$ @PeterCordes Ah, that'd skip the square root step - nice one! \$\endgroup\$
    – Graham
    Commented Oct 10 at 6:30
2
\$\begingroup\$

If you don't have a way to get the high half of a 16bit multiplication then you can instead scale the vector back before multiplying. (s and c are your byte-sized sin and cos)

x = (x>>8) * c - (y>>8) * s;
y = (x>>8) * s + (y>>8) * c;

That will end up canceling out the upscale of the sin and cos.

If you don't mind a bit more operations per rotation you can also compute the bit you cut off:

x = (x>>8 * c  + ((x<0?-1:1)*(x&x0ff)*c)>>8 ) - (y>>8*s + ((y<0?-1:1)*(y&0xff)*s)>>8 );
y = (x>>8 * s  + ((x<0?-1:1)*(x&x0ff)*s)>>8 ) + (y>>8*c + ((y<0?-1:1)*(y&0xff)*c)>>8 );
\$\endgroup\$
3
  • 4
    \$\begingroup\$ You're throwing away half your input precision, though. \$\endgroup\$ Commented Oct 9 at 16:11
  • \$\begingroup\$ @user2357112 fixed in the edit \$\endgroup\$ Commented Oct 10 at 8:25
  • \$\begingroup\$ @ratchetfreak: You can fix even more, if I understand the problem right. The ">>8" can actually be ">>7", since the s/c are signed, therefore having only 7 relevant bits, sign not counted. \$\endgroup\$
    – virolino
    Commented Oct 10 at 8:35
1
\$\begingroup\$

I think the trick here is to use the fact that the hardware is usually more capable than the compiler exposes. In particular, when you multiply a 16 bit value by a 16 bit value, you will get a 32 bit value, but the compiler will immediately throw away the upper half (unless you are looking at its internal library to do 32 bit math).

So what you want to do is get your sin(),cos() values in the -32768 ... 32767 range (by multiplying by 32768 and special casing the overflow), and then do the signed multiply, left shift the resultant register pair one bit left (typically rotate-through-carry-left or RCL), and then return the upper half of the result. You might also benefit from using the unsigned multiply operations, but then you need to track the signs yourself.

\$\endgroup\$
1
\$\begingroup\$

So...

  • you do not want to use 32 bits per integer;
  • you do not want to use float (or double);
  • you do not want to lose precision.

That cannot be done for the full range of numbers, but the precision can be dealt with, respecting the first 2 conditions 100%.


The trick is that:

  • x and y are both max 15 bits + 1 bit sign;
  • sine and cos are both max 7 bits + 1 bit sign;
  • the result is max 15 bits + 1 bit sign.

To speed things up a bit, you need to keep a parallel table with the LUT for sin and cos, to know the number of relevant bits of each value (leading zeros are irrelevant).

And somehow you find the number of relevant bits for x and y (separately).

An now the magic: If the result has 15 bits (max), and the s / c has k bits, and x / y has q bits, you need to scale the number(*) by (k+q-15) if (k+q) is bigger than 15, perform the multiplication, and then reverse scale the result (if needed / desired).

I hope that my explanation makes sense.

(*) I recommend to scale the bigger number, to keep the precision of the smaller number. Just compare e.g., x to sin, and scale whichever is bigger. Otherwise you risk to transform a non-zero number to a "very" zero, which is more undesirable than some loss of precision.


It might be the case that the resulting coordinate will be out of screen / out of visible area. In this case, just by counting bits you might find out if the resulting coordinates are worth calculating (with any precision) or not at all.


Disclaimer: you will lose some speed, though... It is in your responsibility to fond the sweet spot of the optimization. Trial and error, I guess...

\$\endgroup\$
0
\$\begingroup\$

First of all, as Kromster commented, you need to use immediate variables in order to not accidentally use the new x value when calculating the new y value, unless you're actually using a vectorized version. Second, when you say your sin/cos is encoded in the range -127..127 (shouldn't one of them be 128?) your formula needs to include the rescaling and should not call the encoded values sin/cos since those must be the proper floats in the -1..1 range:

x_new = (x*ct - y*st)/TRIG_SCALE
y_new = (x*st + y*ct)/TRIG_SCALE

where TRIG_SCALE=127, ct = TRIG_SCALE*cos(theta) and st=TRIG_SCALE*sin(theta).

Next, you can split x and y into to 8-bit parts each and their sign, such that x = sign_x *(x_high * 256 + x_low) (and analogously for y). Then

x_new = (sign_x*(x_high*256 + x_low)*ct - sign_y*(y_high*256 + y_low)*st) / TRIG_SCALE
      =   (sign_x*sign_ct*x_high*abs(ct) - sign_y*sign_st*y_high*abs(st))*256/TRIG_SCALE
        + (sign_x*sign_ct*x_low*abs(ct)  - sign_y*sign_st*y_low*abs(st))/TRIG_SCALE

and similar for y_new. Both parentheses in the second and third line are limited to 14/15 bit plus sign (7/8 bit from high/low bits plus 7 bit from abs(cos/sin)) so they can be handled individually via two 16 bit variables per coordinate which can then be individually scaled down by TRIG_SCALE before the addition. Note that since 256/TRIG_SCALE=2 the high part actually just gets doubled, thus yielding a 15 bit number as well. For the low part you'll loose precision due to the integer division, but that's to be expected here. Due to the mathematical nature of sin/cos those two 15 bit numbers shouldn't overflow when added, but I'll leave proving that as an exercise to the reader ;)

\$\endgroup\$
9
  • 1
    \$\begingroup\$ -128 is the lower bound on a signed byte, but was probably excluded to maintain an even spacing while looping either direction. And this answer very similar to ratchet freaks: >>8 for integers is the same as ` / 256` and <<8 is * 2^8 or * 256 only MUCH faster. your answer does attempt to restore the lower precision. \$\endgroup\$
    – agone
    Commented Oct 9 at 21:58
  • \$\begingroup\$ @agone Yes of course, bit shifts can be used instead of the multiplication, I was trying to make the answer more readable and any decent compiler automatically takes care of this \$\endgroup\$ Commented Oct 10 at 16:34
  • 1
    \$\begingroup\$ @agone all modern compilers convert /256 to >> 8 and * 256 to << 8. \$\endgroup\$
    – Questor
    Commented Oct 10 at 17:28
  • 1
    \$\begingroup\$ I have used a 30 year old compiler--which was the worst compilers I have ever worked with-that had that optimization implemented. Most of the time, a compiler already does the optimization you just thought of, and often it does a much better optimization. If you don't trust the compiler's optimization for the low hanging fruit, you cannot trust it to optimize anything, and so should just write assembly, if performance/size matters. \$\endgroup\$
    – Questor
    Commented Oct 15 at 18:38
  • 1
    \$\begingroup\$ @Questor, "If you can't understand assembly then you can't beat the compiler." No truer words were spoken. Write high level, profile, optimize. My tight loops start in ASM. Xilinx GCC(a front end filter, ) is horrible: The point of using software programming like C is dynamic memory allocation/loop size pocessing, attempting fixed size, parallel synthesis of unknown runtime factors in hardware is a mistake. Use VHDL or Verilog to save on ASIC fabrication costs. \$\endgroup\$
    – agone
    Commented Oct 20 at 16:15

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .