Skip to content

Commit cfed620

Browse files
committed
more affine
1 parent eaa6b3c commit cfed620

4 files changed

Lines changed: 329 additions & 0 deletions

File tree

gf2p8affineqb/gf2p8affineqb-visualizer.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,11 @@
5959
.gf2p8-cpp-section {
6060
margin-top: 15px;
6161
}
62+
63+
.gf2p8-blend-section {
64+
margin-top: 10px;
65+
}
66+
67+
.gf2p8-blend-section p {
68+
margin: 6px 0;
69+
}

gf2p8affineqb/gf2p8affineqb-visualizer.js

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,216 @@ assert(resultByte == ${expectedResult});`;
255255
container.appendChild(cppSection);
256256
}
257257

258+
/**
259+
* The matrix RPCS3 uses to decode SHUFB's special-case control bytes.
260+
* Byte 0 (the row for output bit 7) routes control bit 6 to the sign bit,
261+
* bytes 1-7 (rows for output bits 6..0) route control bit 5 everywhere else.
262+
*/
263+
export const SHUFB_GFNI_MATRIX = [0x40, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20];
264+
265+
/**
266+
* Create a visualization of RPCS3's SHUFB special-case fast path:
267+
* one GF2P8AFFINEQB over the SHUFB control byte, followed by two blends.
268+
* @param {HTMLElement} container - The container element to render into
269+
* @param {{control: number, matrix: Uint8Array, imm: number}} state - Mutable simulator state
270+
*/
271+
export function createShufbVisualization(container, state) {
272+
const previousDetails = container.querySelector('details');
273+
const wasOpen = previousDetails ? previousDetails.open : false;
274+
275+
container.innerHTML = '';
276+
277+
const rerender = () => createShufbVisualization(container, state);
278+
279+
let matrixQword = 0n;
280+
for (let i = 0; i < 8; i++) {
281+
matrixQword |= BigInt(state.matrix[i]) << BigInt(i * 8);
282+
}
283+
284+
const gfni = affineByte(matrixQword, state.control, state.imm);
285+
const idxConst = (gfni & 0x80) ? gfni : 0x00;
286+
const controlIsSpecial = (state.control & 0x80) !== 0;
287+
288+
const hex = (x) => '0x' + x.toString(16).padStart(2, '0').toUpperCase();
289+
const bin = (x) => '0b' + x.toString(2).padStart(8, '0');
290+
291+
// Main visualization: matrix × control + imm = gfni
292+
const vizContainer = document.createElement('div');
293+
vizContainer.classList.add('gf2p8-viz-container');
294+
295+
// Matrix grid
296+
const matrixGrid = document.createElement('div');
297+
matrixGrid.classList.add('gf2p8-matrix-grid');
298+
for (let row = 7; row >= 0; row--) {
299+
for (let col = 7; col >= 0; col--) {
300+
const byteIndex = (7 - row);
301+
const bit = (state.matrix[byteIndex] >> col) & 1;
302+
const cell = document.createElement('div');
303+
cell.textContent = bit;
304+
cell.classList.add('gf2p8-cell', 'interactive', bit ? 'one' : 'zero');
305+
cell.addEventListener('click', () => {
306+
state.matrix[byteIndex] ^= (1 << col);
307+
rerender();
308+
});
309+
matrixGrid.appendChild(cell);
310+
}
311+
}
312+
vizContainer.appendChild(matrixGrid);
313+
314+
const xLabel = document.createElement('div');
315+
xLabel.textContent = '×';
316+
xLabel.classList.add('gf2p8-operator');
317+
vizContainer.appendChild(xLabel);
318+
319+
// Control byte vector
320+
const controlGrid = document.createElement('div');
321+
controlGrid.classList.add('gf2p8-vector-grid');
322+
for (let bit = 7; bit >= 0; bit--) {
323+
const bitValue = (state.control >> bit) & 1;
324+
const cell = document.createElement('div');
325+
cell.textContent = bitValue;
326+
cell.classList.add('gf2p8-cell', 'interactive', bitValue ? 'one' : 'zero');
327+
cell.addEventListener('click', () => {
328+
state.control ^= (1 << bit);
329+
rerender();
330+
});
331+
controlGrid.appendChild(cell);
332+
}
333+
vizContainer.appendChild(controlGrid);
334+
335+
const xorLabel = document.createElement('div');
336+
xorLabel.textContent = '+';
337+
xorLabel.classList.add('gf2p8-operator');
338+
vizContainer.appendChild(xorLabel);
339+
340+
// Immediate constant vector
341+
const immGrid = document.createElement('div');
342+
immGrid.classList.add('gf2p8-vector-grid');
343+
for (let bit = 7; bit >= 0; bit--) {
344+
const bitValue = (state.imm >> bit) & 1;
345+
const cell = document.createElement('div');
346+
cell.textContent = bitValue;
347+
cell.classList.add('gf2p8-cell', 'interactive', bitValue ? 'one' : 'zero');
348+
cell.addEventListener('click', () => {
349+
state.imm ^= (1 << bit);
350+
rerender();
351+
});
352+
immGrid.appendChild(cell);
353+
}
354+
vizContainer.appendChild(immGrid);
355+
356+
const equalsLabel = document.createElement('div');
357+
equalsLabel.textContent = '=';
358+
equalsLabel.classList.add('gf2p8-operator');
359+
vizContainer.appendChild(equalsLabel);
360+
361+
// GFNI result with annotations
362+
const resultGrid = document.createElement('div');
363+
resultGrid.classList.add('gf2p8-result-grid');
364+
for (let bit = 7; bit >= 0; bit--) {
365+
const bitValue = (gfni >> bit) & 1;
366+
const cell = document.createElement('div');
367+
cell.textContent = bitValue;
368+
cell.classList.add('gf2p8-cell', bitValue ? 'result-one' : 'zero');
369+
resultGrid.appendChild(cell);
370+
371+
const annotation = document.createElement('div');
372+
annotation.classList.add('gf2p8-annotation');
373+
const matrixByte = state.matrix[7 - bit];
374+
const sourceBits = [];
375+
for (let i = 7; i >= 0; i--) {
376+
if ((matrixByte >> i) & 1) {
377+
sourceBits.push(`control bit ${i}`);
378+
}
379+
}
380+
const immBit = (state.imm >> bit) & 1;
381+
if (sourceBits.length === 0) {
382+
annotation.textContent = immBit ? '1' : '0';
383+
} else if (immBit) {
384+
annotation.textContent = `¬(${sourceBits.join(' + ')})`;
385+
} else {
386+
annotation.textContent = sourceBits.join(' + ');
387+
}
388+
resultGrid.appendChild(annotation);
389+
}
390+
vizContainer.appendChild(resultGrid);
391+
392+
container.appendChild(vizContainer);
393+
394+
// Blend fixup steps
395+
const blendSection = document.createElement('div');
396+
blendSection.classList.add('gf2p8-blend-section');
397+
398+
const step1 = document.createElement('p');
399+
step1.innerHTML =
400+
`<b>Blend 1</b> — force <code>0x00</code> when the affine result's sign bit is clear: ` +
401+
`<code>gfni = ${bin(gfni)}</code>, sign bit ${(gfni & 0x80) ? 'set, kept as' : 'clear, forced to'} ` +
402+
`<code>${hex(idxConst)}</code>`;
403+
blendSection.appendChild(step1);
404+
405+
const step2 = document.createElement('p');
406+
if (controlIsSpecial) {
407+
step2.innerHTML =
408+
`<b>Blend 2</b> — the control byte's own sign bit is set, so the special-case constant is selected: ` +
409+
`result byte = <code>${hex(idxConst)}</code>`;
410+
} else {
411+
step2.innerHTML =
412+
`<b>Blend 2</b> — the control byte's own sign bit is clear, so the shuffle result is selected: ` +
413+
`result byte = byte ${state.control & 0x1F} of RA‖RB (the affine result is discarded)`;
414+
}
415+
blendSection.appendChild(step2);
416+
417+
const verdict = document.createElement('p');
418+
let expected;
419+
if (!controlIsSpecial) {
420+
expected = `byte ${state.control & 0x1F} of RA‖RB`;
421+
} else if ((state.control & 0xC0) === 0x80) {
422+
expected = '0x00';
423+
} else if ((state.control & 0xE0) === 0xC0) {
424+
expected = '0xFF';
425+
} else {
426+
expected = '0x80';
427+
}
428+
const actual = controlIsSpecial ? hex(idxConst) : `byte ${state.control & 0x1F} of RA‖RB`;
429+
const matches = actual === expected;
430+
verdict.innerHTML =
431+
`Control byte <code>${bin(state.control)}</code>: <i>Table 5-1</i> says <code>${expected}</code>, ` +
432+
`this path produces <code>${actual}</code> ${matches ? '✓' : '✗ (you broke it — reset the matrix to 0x2020202020202040 and imm8 to 0x7F)'}`;
433+
blendSection.appendChild(verdict);
434+
435+
container.appendChild(blendSection);
436+
437+
// C++ code example
438+
const cppSection = document.createElement('details');
439+
cppSection.classList.add('gf2p8-cpp-section');
440+
441+
const cppSummary = document.createElement('summary');
442+
cppSummary.textContent = 'C++ implementation';
443+
cppSummary.classList.add('gf2p8-cpp-summary');
444+
cppSection.appendChild(cppSummary);
445+
446+
const cppCode = document.createElement('pre');
447+
cppCode.classList.add('gf2p8-cpp-code');
448+
const codeElement = document.createElement('code');
449+
codeElement.textContent = `// ${'0x' + matrixQword.toString(16).padStart(16, '0')}
450+
const __m128i k_matrix = _mm_set1_epi64x(0x${matrixQword.toString(16).padStart(16, '0')});
451+
452+
// c holds the 16 SHUFB control bytes, shuffled holds the bytes already
453+
// gathered from RA||RB (e.g. via VPERM2B)
454+
const __m128i gfni = _mm_gf2p8affine_epi64_epi8(c, k_matrix, ${hex(state.imm)});
455+
456+
// Blend 1: where gfni's sign bit is clear, force 0x00 (fixes up 10xxxxxx)
457+
const __m128i idx_consts = _mm_blendv_epi8(_mm_setzero_si128(), gfni, gfni);
458+
459+
// Blend 2: where the control byte's sign bit is set, take the special
460+
// constant; otherwise take the shuffled data
461+
const __m128i result = _mm_blendv_epi8(shuffled, idx_consts, c);`;
462+
cppCode.appendChild(codeElement);
463+
cppSection.appendChild(cppCode);
464+
cppSection.open = wasOpen;
465+
container.appendChild(cppSection);
466+
}
467+
258468
/**
259469
* Create a matrix from bytes for visualization
260470
* @param {number[]} bytes - 8 bytes representing the matrix rows

gf2p8affineqb/gf2p8affineqb.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,36 @@ describe('gf2p8affineqb', () => {
174174
expect(result_neg[0]).toBe(0b11111111); // Sign extended
175175
});
176176
});
177+
178+
describe("RPCS3 SHUFB special-case fast path (Whatcookie's trick)", () => {
179+
// Matrix used in RPCS3's SPULLVMRecompiler SHUFB path:
180+
// Byte 0 (row for output bit 7): 0x40 -> copies control bit 6 to the sign bit
181+
// Bytes 1-7 (rows for output bits 6..0): 0x20 -> copy control bit 5,
182+
// which imm8 = 0x7F then inverts
183+
const shufbMatrix = 0x2020202020202040n;
184+
const imm8 = 0x7f;
185+
186+
/** Model the affine op followed by RPCS3's first blend (force 0x00 when sign bit clear) */
187+
function idxConst(control) {
188+
const gfni = affineByte(shufbMatrix, control, imm8);
189+
return (gfni & 0x80) ? gfni : 0x00;
190+
}
191+
192+
test('10xxxxxx produces 0x00', () => {
193+
for (let c = 0b10000000; c <= 0b10111111; c++) {
194+
expect(idxConst(c)).toBe(0x00);
195+
}
196+
});
197+
198+
test('110xxxxx produces 0xFF', () => {
199+
for (let c = 0b11000000; c <= 0b11011111; c++) {
200+
expect(idxConst(c)).toBe(0xFF);
201+
}
202+
});
203+
204+
test('111xxxxx produces 0x80', () => {
205+
for (let c = 0b11100000; c <= 0b11111111; c++) {
206+
expect(idxConst(c)).toBe(0x80);
207+
}
208+
});
209+
});

gf2p8affineqb/index.html

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,84 @@ <h2>Shuffle Bytes</h2>
621621
end</pre>
622622
</blockquote>
623623

624+
<p>
625+
A naive implementation has to check for the three constant-generating patterns
626+
(<code>10xxxxxx</code><code>0x00</code>, <code>110xxxxx</code><code>0xFF</code>,
627+
<code>111xxxxx</code><code>0x80</code>) on every one of the 16 control bytes, on top of
628+
the actual shuffle. <a href="https://github.com/Whatcookie">Whatcookie</a>'s fast path
629+
(<a href="https://github.com/RPCS3/rpcs3/pull/8712">RPCS3 PR #8712</a>) decodes all three
630+
special cases at once with a single GF2P8AFFINEQB over the control bytes, because each
631+
output is a pure boolean function of control bits 5, 6, and 7 &mdash; exactly the kind of
632+
function an affine transformation can compute:
633+
</p>
634+
635+
<ul>
636+
<li>The matrix row for output bit 7 is <code>0b01000000</code>, so the result's sign bit is a copy
637+
of control bit 6.</li>
638+
<li>The rows for output bits 6 through 0 are all <code>0b00100000</code>, selecting control bit
639+
5, and imm8 = <code>0x7F</code> flips those seven bits, so they become ¬(control bit 5).</li>
640+
</ul>
641+
642+
<p>
643+
Two cheap blends then finish the job: one forces the byte to <code>0x00</code> wherever the affine
644+
result's sign bit is clear (fixing up the <code>10xxxxxx</code> case), and one selects between the
645+
special-case constant and the actual shuffled data based on the control byte's own sign bit.
646+
Toggle the control byte's top three bits below and watch <i>Table 5-1</i> fall out:
647+
</p>
648+
649+
<div id="viz-shufb" class="simulator-container"></div>
650+
651+
<script type="module">
652+
import { createShufbVisualization, SHUFB_GFNI_MATRIX } from './gf2p8affineqb-visualizer.js';
653+
654+
createShufbVisualization(document.getElementById('viz-shufb'), {
655+
control: 0b11000101,
656+
matrix: new Uint8Array(SHUFB_GFNI_MATRIX),
657+
imm: 0x7f,
658+
});
659+
</script>
660+
661+
<p>
662+
Here's the actual source, from RPCS3's SPU LLVM recompiler
663+
(<code>gf2p8affineqb</code> is a thin wrapper that emits the
664+
<code>llvm.x86.vgf2p8affineqb.128</code> intrinsic, and <code>c</code> holds the 16 SHUFB
665+
control bytes). The 16-byte <code>build</code> is the <code>0x2020202020202040</code> matrix
666+
from the simulator above, once per 64-bit lane:
667+
</p>
668+
669+
<figure>
670+
<pre><code>// Calculate special index constants
671+
672+
value_t&lt;u8[16]&gt; idx_consts;
673+
if (perm_or_zero_only)
674+
{
675+
idx_consts = eval(splat&lt;u8[16]&gt;(0));
676+
}
677+
else if (m_use_avx512_icl)
678+
{
679+
const auto gfni = gf2p8affineqb(c, build&lt;u8[16]&gt;(0x40, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x40, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20), 0x7f);
680+
idx_consts = eval(select(noncast&lt;s8[16]&gt;(gfni) &gt;= 0, splat&lt;u8[16]&gt;(0), gfni));
681+
}
682+
else
683+
{
684+
const auto pshufb_lut = build&lt;u8[16]&gt;(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x80, 0x80);
685+
idx_consts = eval(pshufb(pshufb_lut, (c &gt;&gt; 4)));
686+
}
687+
688+
// Combine shuffle and special index constants
689+
690+
if (shuf_zero_when_msb)
691+
set_vr(op.rt4, ab_shuf | idx_consts);
692+
else
693+
set_vr(op.rt4, select(noncast&lt;s8[16]&gt;(c) &gt;= 0, ab_shuf, idx_consts));</code></pre>
694+
<figcaption>
695+
<a
696+
href="https://github.com/RPCS3/rpcs3/blob/cdbc43712bac68cd174f4c7305bfcd41d04db51c/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp#L7635-L7658">
697+
<cite>rpcs3/Emu/Cell/SPULLVMRecompiler.cpp</cite>, lines 7635&ndash;7658, in <code>SHUFB</code>
698+
</a>
699+
</figcaption>
700+
</figure>
701+
624702
<p>
625703
<a href="https://www.youtube.com/watch?v=19ae5Mq2lJE">Here's the video that discusses it</a>
626704
</p>

0 commit comments

Comments
 (0)