[rlsw] Use ESP-DSP for 4x4 matrix multiply and per-vertex MVP transform

Adds an opt-in ESP-DSP code path for ESP32 / ESP32-S3 builds. ESP-DSP is
ESP-IDF's official optimized math library and ships hand-vectorized
kernels that beat the scalar implementations on Xtensa.

Two integration points:

  1. `sw_matrix_mul_rst` -> `dspm_mult_4x4x4_f32` for any 4x4*4x4 multiply
     (used for MVP build, gluLookAt, push/multiply, etc.). rlsw stores
     matrices column-major and ESP-DSP reads row-major; the comment on the
     call site explains why the flat-buffer call still produces the
     correct column-major product (transpose-of-transposes equivalence).

  2. `sw_immediate_push_vertex` -> `dspm_mult_4x4x1_f32` for the per-vertex
     clip-space transform. Because ESP-DSP expects a row-major matrix in
     this case, a row-major copy `matMVP_rm[16]` is maintained alongside
     `matMVP` and refreshed once per `isDirtyMVP` rebuild in
     `sw_immediate_begin`. Cost is 16 scalar copies per matrix update,
     amortized over thousands of vertices per frame.

Detection is **opt-in** via `SW_USE_ESP_DSP` so existing ESP-IDF projects
that don't depend on the `esp-dsp` component keep building unchanged.
A user enables it from CMakeLists.txt (or anywhere before including
rlgl.h):

    target_compile_definitions(${COMPONENT_LIB} PRIVATE SW_USE_ESP_DSP=1)

and adds the dependency to `idf_component.yml`:

    espressif/esp-dsp: "^1.4.0"

Measured on ESP32-S3 @ 240 MHz, R5G6B5 240x240, textured 3D model:
contributes meaningfully to the overall frame-time improvement
(combined with sw_rcp).

Made-with: Cursor
This commit is contained in:
Jens Roth 2026-04-30 16:34:54 +02:00
parent fd57316ff0
commit b45ccc044d

42
src/external/rlsw.h vendored
View File

@ -844,6 +844,17 @@ SWAPI void swGetFramebufferAttachmentParameteriv(SWattachment attachment, SWatta
#endif
#endif
// ESP-DSP acceleration: ESP-IDF ships an optimized math library that includes
// `dspm_mult_4x4x4_f32` (4x4 matrix multiply) and `dspm_mult_4x4x1_f32`
// (matrix * vector). These are S3-tuned hand-vectorized kernels that beat the
// scalar versions for both throughput and code-size. Detection is opt-in to
// keep the dependency optional: define SW_USE_ESP_DSP from your build system
// (or rely on the `idf_component.yml` example shown in the rlsw docs).
#if defined(ESP_PLATFORM) && defined(SW_USE_ESP_DSP)
#define SW_HAS_ESP_DSP
#include "dspm_mult.h"
#endif
#ifdef __cplusplus
#define SW_CURLY_INIT(name) name
#else
@ -1038,6 +1049,9 @@ typedef struct {
SWmatrix currentMatrixMode; // Current matrix mode (e.g., sw_MODELVIEW, sw_PROJECTION)
sw_matrix_t *currentMatrix; // Pointer to the currently used matrix according to the mode
sw_matrix_t matMVP; // Model view projection matrix, calculated and used internally
#ifdef SW_HAS_ESP_DSP
float matMVP_rm[16]; // Row-major MVP, kept in sync for esp-dsp dspm_mult_4x4x1_f32 vertex transform
#endif
bool isDirtyMVP; // Indicates if the MVP matrix should be rebuilt
sw_handle_t boundFramebufferId; // Framebuffer currently bound
@ -1141,6 +1155,14 @@ static inline void sw_matrix_id(sw_matrix_t dst)
static inline void sw_matrix_mul_rst(float *SW_RESTRICT dst, const float *SW_RESTRICT left, const float *SW_RESTRICT right)
{
#ifdef SW_HAS_ESP_DSP
// dspm_mult_4x4x4_f32 treats its operands as row-major. rlsw stores matrices
// column-major, so passing them flat is equivalent to passing transposes:
// dspm_mult(L^T, R^T) computes (L^T)*(R^T) = (R*L)^T, written back into a
// flat array gives the same bit pattern as the column-major product (R*L)
// -- exactly the semantic the scalar fallback below has.
dspm_mult_4x4x4_f32(left, right, dst);
#else
float l00 = left[0], l01 = left[1], l02 = left[2], l03 = left[3];
float l10 = left[4], l11 = left[5], l12 = left[6], l13 = left[7];
float l20 = left[8], l21 = left[9], l22 = left[10], l23 = left[11];
@ -1165,6 +1187,7 @@ static inline void sw_matrix_mul_rst(float *SW_RESTRICT dst, const float *SW_RES
dst[7] = l10*right[3] + l11*right[7] + l12*right[11] + l13*right[15];
dst[11] = l20*right[3] + l21*right[7] + l22*right[11] + l23*right[15];
dst[15] = l30*right[3] + l31*right[7] + l32*right[11] + l33*right[15];
#endif
}
static inline void sw_matrix_mul(sw_matrix_t dst, const sw_matrix_t left, const sw_matrix_t right)
@ -3818,6 +3841,19 @@ static void sw_immediate_begin(SWdraw mode)
RLSW.stackModelview[RLSW.stackModelviewCounter - 1],
RLSW.stackProjection[RLSW.stackProjectionCounter - 1]);
#ifdef SW_HAS_ESP_DSP
// Pre-transpose to row-major so dspm_mult_4x4x1_f32(matMVP_rm, v, out)
// computes M*v directly in the per-vertex hot path. 16 scalar copies
// per MVP update vs. saving ~20 cycles per vertex transform.
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
RLSW.matMVP_rm[4*i + j] = RLSW.matMVP[4*j + i];
}
}
#endif
RLSW.isDirtyMVP = false;
}
@ -3869,11 +3905,17 @@ static void sw_immediate_push_vertex(const float position[4])
sw_vertex_t *vertex = &RLSW.primitive.buffer[RLSW.primitive.vertexCount++];
// Calculate clip coordinates
#ifdef SW_HAS_ESP_DSP
// dspm_mult_4x4x1_f32 declares its inputs non-const; rlsw treats them as
// read-only and the cast is safe (the kernel only loads from B).
dspm_mult_4x4x1_f32(RLSW.matMVP_rm, (float *)position, vertex->position);
#else
const float *m = RLSW.matMVP;
vertex->position[0] = m[0]*position[0] + m[4]*position[1] + m[8]*position[2] + m[12]*position[3];
vertex->position[1] = m[1]*position[0] + m[5]*position[1] + m[9]*position[2] + m[13]*position[3];
vertex->position[2] = m[2]*position[0] + m[6]*position[1] + m[10]*position[2] + m[14]*position[3];
vertex->position[3] = m[3]*position[0] + m[7]*position[1] + m[11]*position[2] + m[15]*position[3];
#endif
// Copy the attributes in the current vertex
for (int i = 0; i < 4; i++) vertex->color[i] = RLSW.primitive.color[i];