From b45ccc044dd86bb7fb8c260f18c490d4d2704d4c Mon Sep 17 00:00:00 2001 From: Jens Roth <98889125+jensroth-git@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:34:54 +0200 Subject: [PATCH] [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 --- src/external/rlsw.h | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/external/rlsw.h b/src/external/rlsw.h index a48ca70a1..3324f969c 100644 --- a/src/external/rlsw.h +++ b/src/external/rlsw.h @@ -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];