From 2b6a8ffba2129d6f7ae7313375200f9906ce51df Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 10 Jul 2026 21:58:03 -0400 Subject: [PATCH 001/408] rbutil: Get rid of all remnants of SAPI4 TTS support Change-Id: I89cc53e2ebea273752c119e8c63f7eb36b1f22e0 --- tools/sapi_voice.vbs | 204 +++++++++-------------------- utils/rbutilqt/base/rbsettings.cpp | 1 - utils/rbutilqt/base/rbsettings.h | 1 - 3 files changed, 65 insertions(+), 141 deletions(-) diff --git a/tools/sapi_voice.vbs b/tools/sapi_voice.vbs index f39be986d0..4d81eda4a6 100644 --- a/tools/sapi_voice.vbs +++ b/tools/sapi_voice.vbs @@ -37,12 +37,11 @@ Const STDERR = 2 Dim oShell, oArgs, oEnv Dim oFSO, oStdIn, oStdOut -Dim bVerbose, bSAPI4, bList +Dim bVerbose, bList Dim bMSSP Dim sLanguage, sVoice, sSpeed, sName, sVendor Dim oSpVoice, oSpFS ' SAPI5 voice and filestream -Dim oTTS, nMode ' SAPI4 TTS object, mode selector Dim oVoice ' for traversing the list of voices Dim nLangID, sSelectString @@ -59,142 +58,78 @@ Set oEnv = oShell.Environment("Process") bVerbose = (oEnv("V") <> "") Set oArgs = WScript.Arguments.Named -bSAPI4 = oArgs.Exists("sapi4") bMSSP = oArgs.Exists("mssp") bList = oArgs.Exists("listvoices") sLanguage = oArgs.Item("language") sVoice = oArgs.Item("voice") sSpeed = oArgs.Item("speed") +' Create SAPI5 object +If bMSSP Then + Set oSpVoice = CreateObject("speech.SpVoice") +Else + Set oSpVoice = CreateObject("SAPI.SpVoice") +End If +If Err.Number <> 0 Then + WScript.StdErr.WriteLine "Error " & Err.Number _ + & " - could not get SpVoice object." _ + & " SAPI 5 not installed?" + WScript.Quit 1 +End If -If bSAPI4 Then - ' Create SAPI4 ActiveVoice object - Set oTTS = WScript.CreateObject("ActiveVoice.ActiveVoice", "TTS_") - If Err.Number <> 0 Then - Err.Clear - Set oTTS = WScript.CreateObject("ActiveVoice.ActiveVoice.1", "TTS_") - If Err.Number <> 0 Then - WScript.StdErr.WriteLine "Error - could not get ActiveVoice" _ - & " object. SAPI 4 not installed?" - WScript.Quit 1 - End If - End If - oTTS.Initialized = 1 - - If bList Then - ' Just list available voices for the selected language - For Each nLangID in LangIDs(sLanguage) - For nMode = 1 To oTTS.CountEngines - If oTTS.LanguageID(nMode) = nLangID Then - WScript.StdErr.Write oTTS.ModeName(nMode) & "," - End If - Next - Next - WScript.StdErr.WriteLine - WScript.Quit 0 - End If - - ' Select matching voice - For Each nLangID in LangIDs(sLanguage) - sSelectString = "LanguageID=" & nLangID - If sVoice <> "" Then - sSelectString = sSelectString & ";Speaker=" & sVoice _ - & ";ModeName=" & sVoice - End If - nMode = oTTS.Find(sSelectString) - If oTTS.LanguageID(nMode) = nLangID And (sVoice = "" Or _ - oTTS.Speaker(nMode) = sVoice Or oTTS.ModeName(nMode) = sVoice) Then - sName = oTTS.ModeName(nMode) - If bVerbose Then - WScript.StdErr.WriteLine "Using " & sName & " for " & sSelectString - End If - Exit For - Else - sSelectString = "" - End If - Next - If sSelectString = "" Then - WScript.StdErr.WriteLine "Error - found no matching voice for " _ - & sLanguage & ", " & sVoice - WScript.Quit 1 - End If - oTTS.Select nMode - - ' Speed selection - If sSpeed <> "" Then oTTS.Speed = sSpeed - - ' Get vendor information - sVendor = oTTS.MfgName(nMode) - -Else ' SAPI5 - ' Create SAPI5 object - If bMSSP Then - Set oSpVoice = CreateObject("speech.SpVoice") - Else - Set oSpVoice = CreateObject("SAPI.SpVoice") - End If - If Err.Number <> 0 Then - WScript.StdErr.WriteLine "Error " & Err.Number _ - & " - could not get SpVoice object." _ - & " SAPI 5 not installed?" - WScript.Quit 1 - End If - - If bList Then - ' Just list available voices for the selected language - For Each nLangID in LangIDs(sLanguage) - sSelectString = "Language=" & Hex(nLangID) - For Each oVoice in oSpVoice.GetVoices(sSelectString) - WScript.StdErr.Write oVoice.GetAttribute("Name") & ";" - Next - Next - WScript.StdErr.WriteLine - WScript.Quit 0 - End If - - ' Select matching voice +If bList Then + ' Just list available voices for the selected language For Each nLangID in LangIDs(sLanguage) sSelectString = "Language=" & Hex(nLangID) - If sVoice <> "" Then - sSelectString = sSelectString & ";Name=" & sVoice - End If - Set oSpVoice.Voice = oSpVoice.GetVoices(sSelectString).Item(0) - If Err.Number = 0 Then - sName = oSpVoice.Voice.GetAttribute("Name") - If bVerbose Then - WScript.StdErr.WriteLine "Using " & sName & " for " & sSelectString - End If - Exit For - Else - sSelectString = "" - Err.Clear - End If + For Each oVoice in oSpVoice.GetVoices(sSelectString) + WScript.StdErr.Write oVoice.GetAttribute("Name") & ";" + Next Next - If sSelectString = "" Then - WScript.StdErr.WriteLine "Error - found no matching voice for " _ - & sLanguage & ", " & sVoice - WScript.Quit 1 - End If - - ' Speed selection - If sSpeed <> "" Then oSpVoice.Rate = sSpeed - - ' Get vendor information, protect from missing attribute - sVendor = oSpVoice.Voice.GetAttribute("Vendor") - If Err.Number <> 0 Then - Err.Clear - sVendor = "(unknown)" - ' Some L&H engines don't set the vendor attribute - check the name - If Len(sName) > 3 And Left(sName, 3) = "LH " Then - sVendor = "L&H" - End If - End If - - ' Filestream object for output - Set oSpFS = CreateObject("SAPI.SpFileStream") - oSpFS.Format.Type = AudioFormat(sVendor) + WScript.StdErr.WriteLine + WScript.Quit 0 End If +' Select matching voice +For Each nLangID in LangIDs(sLanguage) + sSelectString = "Language=" & Hex(nLangID) + If sVoice <> "" Then + sSelectString = sSelectString & ";Name=" & sVoice + End If + Set oSpVoice.Voice = oSpVoice.GetVoices(sSelectString).Item(0) + If Err.Number = 0 Then + sName = oSpVoice.Voice.GetAttribute("Name") + If bVerbose Then + WScript.StdErr.WriteLine "Using " & sName & " for " & sSelectString + End If + Exit For + Else + sSelectString = "" + Err.Clear + End If +Next +If sSelectString = "" Then + WScript.StdErr.WriteLine "Error - found no matching voice for " _ + & sLanguage & ", " & sVoice + WScript.Quit 1 +End If + +' Speed selection +If sSpeed <> "" Then oSpVoice.Rate = sSpeed +' Get vendor information, protect from missing attribute +sVendor = oSpVoice.Voice.GetAttribute("Vendor") +If Err.Number <> 0 Then + Err.Clear + sVendor = "(unknown)" + ' Some L&H engines don't set the vendor attribute - check the name + If Len(sName) > 3 And Left(sName, 3) = "LH " Then + sVendor = "L&H" + End If +End If + +' Filestream object for output +Set oSpFS = CreateObject("SAPI.SpFileStream") +oSpFS.Format.Type = AudioFormat(sVendor) + Do aLine = Split(oStdIn.ReadLine, vbTab, 2) If Err.Number <> 0 Then @@ -211,19 +146,10 @@ Do aData = Split(aLine(1), vbTab, 2) If bVerbose Then WScript.StdErr.WriteLine "Saying " & aData(1) _ & " in " & aData(0) - If bSAPI4 Then - oTTS.FileName = aData(0) - oTTS.Speak aData(1) - While oTTS.Speaking - WScript.Sleep 1 - Wend - oTTS.FileName = "" - Else - oSpFS.Open aData(0), SSFMCreateForWrite, false - Set oSpVoice.AudioOutputStream = oSpFS - oSpVoice.Speak aData(1) - oSpFS.Close - End If + oSpFS.Open aData(0), SSFMCreateForWrite, false + Set oSpVoice.AudioOutputStream = oSpFS + oSpVoice.Speak aData(1) + oSpFS.Close Case "EXEC" If bVerbose Then WScript.StdErr.WriteLine "> " & aLine(1) oShell.Run aLine(1), 0, true diff --git a/utils/rbutilqt/base/rbsettings.cpp b/utils/rbutilqt/base/rbsettings.cpp index 4c4790c513..4e618ea01c 100644 --- a/utils/rbutilqt/base/rbsettings.cpp +++ b/utils/rbutilqt/base/rbsettings.cpp @@ -74,7 +74,6 @@ const static struct { { RbSettings::EncoderPath, ":encoder:/encoderpath", "" }, { RbSettings::EncoderOptions, ":encoder:/encoderoptions", "" }, { RbSettings::CacheDisabled, "cachedisable", "false" }, - { RbSettings::TtsUseSapi4, "sapi/useSapi4", "false" }, { RbSettings::EncoderNarrowBand, ":encoder:/narrowband", "false" }, { RbSettings::WavtrimThreshold, "wavtrimthreshold", "500"}, { RbSettings::TtsSpeed, ":tts:/speed", "175" }, diff --git a/utils/rbutilqt/base/rbsettings.h b/utils/rbutilqt/base/rbsettings.h index 55a4d8e4cf..a94486da20 100644 --- a/utils/rbutilqt/base/rbsettings.h +++ b/utils/rbutilqt/base/rbsettings.h @@ -70,7 +70,6 @@ class RbSettings : public QObject EncoderComplexity, TtsSpeed, CacheDisabled, - TtsUseSapi4, EncoderNarrowBand, EncoderQuality, EncoderVolume, From 4e770b4b878c4c0fda89fa79f67a0481bb7c4597 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 10 Jul 2026 21:57:20 -0400 Subject: [PATCH 002/408] rbutil: Add missing SAPI5 LANGID for Moldavian (ro_MD) Change-Id: Ica67356d9bf71784543e17ea98abc8a8716d6fba --- tools/sapi_voice.vbs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/sapi_voice.vbs b/tools/sapi_voice.vbs index 4d81eda4a6..371c5c5a23 100644 --- a/tools/sapi_voice.vbs +++ b/tools/sapi_voice.vbs @@ -285,6 +285,8 @@ Function LangIDs(ByRef sLanguage) LangIDs = Array(&h427) Case "magyar" LangIDs = Array(&h40e) + Case "moldoveneste" + LangIDs = Array(&h818) Case "nederlands" LangIDs = Array(&h413, &h813) ' Standard, Belgian Case "norsk-nynorsk" From 9ebe34d570a7684dc308a278983c3ec19fee4436 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sat, 11 Jul 2026 08:04:10 -0400 Subject: [PATCH 003/408] speex: Silence spurious build warning when building under rbutil Change-Id: Ida23a960c54de1a46d0787795efb7b28b6427939 --- lib/rbcodec/codecs/libspeex/ltp.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/rbcodec/codecs/libspeex/ltp.c b/lib/rbcodec/codecs/libspeex/ltp.c index 456a184c02..304b12854c 100644 --- a/lib/rbcodec/codecs/libspeex/ltp.c +++ b/lib/rbcodec/codecs/libspeex/ltp.c @@ -813,7 +813,10 @@ spx_word32_t *cumul_gain } for (i=0;i Date: Sat, 11 Jul 2026 08:52:12 -0400 Subject: [PATCH 004/408] fix: new yellow for #pragma GCC diagnostic, GCC must be capitalized. Change-Id: I1d760dd83b4dc29590454f4f4e09c5dface2c48f --- lib/rbcodec/codecs/libspeex/ltp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rbcodec/codecs/libspeex/ltp.c b/lib/rbcodec/codecs/libspeex/ltp.c index 304b12854c..4e4d651d8f 100644 --- a/lib/rbcodec/codecs/libspeex/ltp.c +++ b/lib/rbcodec/codecs/libspeex/ltp.c @@ -813,10 +813,10 @@ spx_word32_t *cumul_gain } for (i=0;i Date: Sat, 11 Jul 2026 12:58:26 +0100 Subject: [PATCH 005/408] echoplayer: fix missing const qualifier on stm32 clocks Change-Id: I295013b5af0e2aad77b5e284b351aaefde42df7c --- firmware/target/arm/stm32/echoplayer/clock-echoplayer.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/firmware/target/arm/stm32/echoplayer/clock-echoplayer.h b/firmware/target/arm/stm32/echoplayer/clock-echoplayer.h index fe296ef6bb..8af8f57e44 100644 --- a/firmware/target/arm/stm32/echoplayer/clock-echoplayer.h +++ b/firmware/target/arm/stm32/echoplayer/clock-echoplayer.h @@ -24,9 +24,9 @@ void echoplayer_clock_init(void) INIT_ATTR; -extern struct stm32_clock sdmmc1_ker_clock; -extern struct stm32_clock ltdc_ker_clock; -extern struct stm32_clock spi5_ker_clock; -extern struct stm32_clock i2c1_ker_clock; +extern const struct stm32_clock sdmmc1_ker_clock; +extern const struct stm32_clock ltdc_ker_clock; +extern const struct stm32_clock spi5_ker_clock; +extern const struct stm32_clock i2c1_ker_clock; #endif /* __CLOCK_ECHOPLAYER_H__ */ From 862370982ca32e6376cd19c4c745e73d59f26b10 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Sat, 11 Jul 2026 11:38:58 +0100 Subject: [PATCH 006/408] stm32h743: implement timer API Change-Id: Idcac3ab528e154097fb76fcfda78540ef02494df --- firmware/SOURCES | 1 + firmware/export/config/echor1.h | 2 +- firmware/reggen/stm32h743.regs | 280 ++++++++++++++++++ .../arm/stm32/echoplayer/clock-echoplayer.c | 8 + .../arm/stm32/echoplayer/clock-echoplayer.h | 1 + .../arm/stm32/echoplayer/system-echoplayer.c | 6 + .../arm/stm32/echoplayer/timer-echoplayer.c | 25 ++ .../target/arm/stm32/irqhandlers-stm32h743.c | 16 + firmware/target/arm/stm32/timer-stm32h7.c | 77 ++++- firmware/target/arm/stm32/timer-stm32h7.h | 31 ++ firmware/target/arm/stm32/vectors-stm32h7.S | 32 +- 11 files changed, 458 insertions(+), 21 deletions(-) create mode 100644 firmware/target/arm/stm32/echoplayer/timer-echoplayer.c create mode 100644 firmware/target/arm/stm32/timer-stm32h7.h diff --git a/firmware/SOURCES b/firmware/SOURCES index 20158f8489..58bc753fc4 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1970,6 +1970,7 @@ target/arm/stm32/echoplayer/pcm-echoplayer.c target/arm/stm32/echoplayer/power-echoplayer.c target/arm/stm32/echoplayer/sdmmc-echoplayer.c target/arm/stm32/echoplayer/system-echoplayer.c +target/arm/stm32/echoplayer/timer-echoplayer.c target/arm/stm32/echoplayer/usb-echoplayer.c #endif diff --git a/firmware/export/config/echor1.h b/firmware/export/config/echor1.h index dd8ad30f66..f6c8760db0 100644 --- a/firmware/export/config/echor1.h +++ b/firmware/export/config/echor1.h @@ -15,7 +15,7 @@ #define CPU_FREQ 480000000 #ifndef SIMULATOR -#define TIMER_FREQ STM32_HSE_FREQ +#define TIMER_FREQ (CPU_FREQ / 2) #endif /* Kernel defines */ diff --git a/firmware/reggen/stm32h743.regs b/firmware/reggen/stm32h743.regs index 03292c347c..8134b08f06 100644 --- a/firmware/reggen/stm32h743.regs +++ b/firmware/reggen/stm32h743.regs @@ -1757,3 +1757,283 @@ block ADC { ADC1 @ 0x40022000 : ADC ADC2 @ 0x40022100 : ADC ADC3 @ 0x58026000 : ADC + +block TIM { + CR1 @ 0x00 : reg { + -- 11 UIFREMAP + 09 08 CKD + -- 07 ARPE + 06 05 CMS + -- 04 DIR + -- 03 OPM + -- 02 URS + -- 01 UDIS + -- 00 CEN + } + + CR2 @ 0x04 : reg { + 23 20 MMS2 + -- 18 OIS6 + -- 16 OIS5 + -- 14 OIS4 + -- 13 OIS3N + -- 12 OIS3 + -- 11 OIS2N + -- 10 OIS2 + -- 09 OIS1N + -- 08 OIS1 + -- 07 TI1S + 06 04 MMS + -- 03 CCDS + -- 02 CCUS + -- 00 CCPC + } + + SMCR @ 0x08 : reg { + 21 20 TS_4_3 + -- 16 SMS3 + -- 15 ETP + -- 14 ECE + 13 12 ETPS + 11 08 ETF + -- 07 MSM + 06 04 TS + 02 00 SMS + } + + DIER @ 0x0c : reg { + -- 14 TDE + -- 13 COMDE + -- 12 CC4DE + -- 11 CC3DE + -- 10 CC2DE + -- 09 CC1DE + -- 08 UDE + -- 07 BIE + -- 06 TIE + -- 05 COMIE + -- 04 CC4IE + -- 03 CC3IE + -- 02 CC2IE + -- 01 CC1IE + -- 00 UIE + } + + SR @ 0x10 : reg { + -- 17 CC6IF + -- 16 CC5IF + -- 13 SBIF + -- 12 CC4OF + -- 11 CC3OF + -- 10 CC2OF + -- 09 CC1OF + -- 08 B2IF + -- 07 BIF + -- 06 TIF + -- 05 COMIF + -- 04 CC4IF + -- 03 CC3IF + -- 02 CC2IF + -- 01 CC1IF + -- 00 UIF + } + + EGR @ 0x14 : reg { + -- 08 B2G + -- 07 BG + -- 06 TG + -- 05 COMG + -- 04 CC4G + -- 03 CC3G + -- 02 CC2G + -- 01 CC1G + -- 00 UG + } + + CCMR1 @ 0x18 : reg { + 09 08 CC2S + 01 00 CC1S + } + + CCMR1_IN @ 0x18 : reg { + 15 12 IC2F + 11 10 IC2PSC + 09 08 CC2S + 07 04 IC1F + 03 02 IC1PSC + 01 00 CC1S + } + + CCMR1_OUT @ 0x18 : reg { + -- 24 OC2M3 + -- 16 OC1M3 + -- 15 OC2CE + 14 12 OC2M + -- 11 OC2PE + -- 10 OC2FE + 09 08 CC2S + -- 07 OC1CE + 06 04 OC1M + -- 03 OC1PE + -- 02 OC1FE + 01 00 CC1S + } + + CCMR2 @ 0x1c : reg { + 09 08 CC4S + 01 00 CC3S + } + + CCMR2_IN @ 0x1c : reg { + 15 12 IC4F + 11 10 IC4PSC + 09 08 CC4S + 07 04 IC3F + 03 02 IC3PSC + 01 00 CC3S + } + + CCMR2_OUT @ 0x1c : reg { + -- 24 OC4M3 + -- 16 OC3M3 + -- 15 OC4CE + 14 12 OC4M + -- 11 OC4PE + -- 10 OC4FE + 09 08 CC4S + -- 07 OC3CE + 06 04 OC3M + -- 03 OC3PE + -- 02 OC3FE + 01 00 CC3S + } + + CCER @ 0x20 : reg { + -- 21 CC6P + -- 20 CC6E + -- 17 CC5P + -- 16 CC5E + -- 15 CC4NP + -- 14 CC4NE + -- 13 CC4P + -- 12 CC4E + -- 11 CC3NP + -- 10 CC3NE + -- 09 CC3P + -- 08 CC3E + -- 07 CC2NP + -- 06 CC2NE + -- 05 CC2P + -- 04 CC2E + -- 03 CC1NP + -- 02 CC1NE + -- 01 CC1P + -- 00 CC1E + } + + CNT @ 0x24 : reg { + -- 31 UIFCPY + 15 00 CNT + } + + CNT32 @ 0x24 : reg { + -- 31 UIFCPY + 30 00 CNT + } + + PSC @ 0x28 : reg + ARR @ 0x2c : reg + RCR @ 0x30 : reg + + CCR1 @ 0x34 : reg + CCR2 @ 0x38 : reg + CCR3 @ 0x3c : reg + CCR4 @ 0x40 : reg + + BDTR @ 0x44 : reg { + -- 25 BK2P + -- 24 BK2E + 23 20 BK2F + 19 16 BKF + -- 15 MOE + -- 14 AOE + -- 13 BKP + -- 12 BKE + -- 11 OSSR + -- 10 OSSI + 09 08 LOCK + 07 00 DTG + } + + DCR @ 0x48 : reg { + 12 08 DBL + 04 00 DBA + } + + DMAR @ 0x4c : reg + + CCMR3_OUT @ 0x54 : reg { + -- 24 OC6M3 + -- 16 OC5M3 + -- 15 OC6CE + 14 12 OC6M + -- 11 OC6PE + -- 10 OC6FE + -- 07 OC5CE + 06 04 OC5M + -- 03 OC5PE + -- 02 OC5FE + } + + CCR5 @ 0x58 : reg { + -- 31 GC5C3 + -- 30 GC5C2 + -- 29 GC5C1 + 15 00 CCR5 + } + + CCR6 @ 0x5c : reg + + AF1 @ 0x60 : reg { + 17 14 ETRSEL + -- 11 BKCMP2P + -- 10 BKCMP1P + -- 09 BKINP + -- 08 BKDFxBKxE // differs for TIM1/TIM8 + -- 02 BKCMP2E + -- 01 BKCMP1E + -- 00 BKINE + } + + AF2 @ 0x64 : reg { + -- 11 BK2CMP2P + -- 10 BK2CMP1P + -- 09 BK2INP + -- 08 BK2DFxBKxE // differs for TIM1/TIM8 + -- 02 BK2CMP2E + -- 01 BK2CMP1E + -- 00 BK2INE + } + + TISEL @ 0x68 : reg { + 27 24 TI4SEL + 19 16 TI3SEL + 11 08 TI2SEL + 03 00 TI1SEL + } +} + +TIM17 @ 0x40014800 : TIM +TIM16 @ 0x40014400 : TIM +TIM15 @ 0x40014000 : TIM +TIM8 @ 0x40010400 : TIM +TIM1 @ 0x40010000 : TIM +TIM14 @ 0x40002000 : TIM +TIM13 @ 0x40001c00 : TIM +TIM12 @ 0x40001800 : TIM +TIM7 @ 0x40001400 : TIM +TIM6 @ 0x40001000 : TIM +TIM5 @ 0x40000c00 : TIM +TIM4 @ 0x40000800 : TIM +TIM3 @ 0x40000400 : TIM +TIM2 @ 0x40000000 : TIM diff --git a/firmware/target/arm/stm32/echoplayer/clock-echoplayer.c b/firmware/target/arm/stm32/echoplayer/clock-echoplayer.c index 59cdc7a2cb..04b469a91d 100644 --- a/firmware/target/arm/stm32/echoplayer/clock-echoplayer.c +++ b/firmware/target/arm/stm32/echoplayer/clock-echoplayer.c @@ -211,3 +211,11 @@ const struct stm32_clock i2c1_ker_clock = { .lpen_reg = ITA_RCC_APB1LLPENR, .lpen_bit = BM_RCC_APB1LENR_I2C1EN, }; + +const struct stm32_clock tim7_ker_clock = { + .frequency = TIMER_FREQ, + .en_reg = ITA_RCC_APB1LENR, + .en_bit = BM_RCC_APB1LENR_TIM7EN, + .lpen_reg = ITA_RCC_APB1LLPENR, + .lpen_bit = BM_RCC_APB1LENR_TIM7EN, +}; diff --git a/firmware/target/arm/stm32/echoplayer/clock-echoplayer.h b/firmware/target/arm/stm32/echoplayer/clock-echoplayer.h index 8af8f57e44..e2d4f0be4a 100644 --- a/firmware/target/arm/stm32/echoplayer/clock-echoplayer.h +++ b/firmware/target/arm/stm32/echoplayer/clock-echoplayer.h @@ -28,5 +28,6 @@ extern const struct stm32_clock sdmmc1_ker_clock; extern const struct stm32_clock ltdc_ker_clock; extern const struct stm32_clock spi5_ker_clock; extern const struct stm32_clock i2c1_ker_clock; +extern const struct stm32_clock tim7_ker_clock; #endif /* __CLOCK_ECHOPLAYER_H__ */ diff --git a/firmware/target/arm/stm32/echoplayer/system-echoplayer.c b/firmware/target/arm/stm32/echoplayer/system-echoplayer.c index 79555f7256..2a5073915c 100644 --- a/firmware/target/arm/stm32/echoplayer/system-echoplayer.c +++ b/firmware/target/arm/stm32/echoplayer/system-echoplayer.c @@ -19,13 +19,16 @@ ****************************************************************************/ #include "system.h" #include "button.h" +#include "nvic-arm.h" #include "gpio-stm32h7.h" +#include "timer-stm32h7.h" #include "clock-echoplayer.h" #include "system-echoplayer.h" #include "regs/stm32h743/fmc.h" #include "regs/stm32h743/pwr.h" #include "regs/stm32h743/rcc.h" #include "regs/stm32h743/rtc.h" +#include "regs/stm32h743/tim.h" #include "regs/cortex-m/cm_scb.h" #ifdef BOOTLOADER @@ -251,6 +254,9 @@ void system_init(void) /* Disable RTC_OUT pin */ echoplayer_set_rtcout_mode(ECHOPLAYER_RTCOUT_DISABLED); + + /* Setup for timer API */ + stm32h7_timer_init(ITA_TIM7, NVIC_IRQN_TIM7, &tim7_ker_clock); } void system_exception_wait(void) diff --git a/firmware/target/arm/stm32/echoplayer/timer-echoplayer.c b/firmware/target/arm/stm32/echoplayer/timer-echoplayer.c new file mode 100644 index 0000000000..845d83e0a6 --- /dev/null +++ b/firmware/target/arm/stm32/echoplayer/timer-echoplayer.c @@ -0,0 +1,25 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 Aidan MacDonald + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include "timer-stm32h7.h" + +void tim7_irq_handler(void) +{ + stm32h7_timer_irq(); +} diff --git a/firmware/target/arm/stm32/irqhandlers-stm32h743.c b/firmware/target/arm/stm32/irqhandlers-stm32h743.c index c9a2d65d90..b40b1bdf2f 100644 --- a/firmware/target/arm/stm32/irqhandlers-stm32h743.c +++ b/firmware/target/arm/stm32/irqhandlers-stm32h743.c @@ -68,6 +68,22 @@ void spi3_irq_handler(void) ATTR_IRQ_HANDLER; void spi4_irq_handler(void) ATTR_IRQ_HANDLER; void spi5_irq_handler(void) ATTR_IRQ_HANDLER; void spi6_irq_handler(void) ATTR_IRQ_HANDLER; +void tim1_brk_irq_handler(void) ATTR_IRQ_HANDLER; +void tim1_cc_irq_handler(void) ATTR_IRQ_HANDLER; +void tim1_trg_irq_handler(void) ATTR_IRQ_HANDLER; +void tim1_up_irq_handler(void) ATTR_IRQ_HANDLER; +void tim2_irq_handler(void) ATTR_IRQ_HANDLER; +void tim3_irq_handler(void) ATTR_IRQ_HANDLER; +void tim4_irq_handler(void) ATTR_IRQ_HANDLER; +void tim6_dac_irq_handler(void) ATTR_IRQ_HANDLER; +void tim7_irq_handler(void) ATTR_IRQ_HANDLER; +void tim8_brk_tim12_irq_handler(void) ATTR_IRQ_HANDLER; +void tim8_cc_irq_handler(void) ATTR_IRQ_HANDLER; +void tim8_trg_tim14_irq_handler(void) ATTR_IRQ_HANDLER; +void tim8_up_tim13_irq_handler(void) ATTR_IRQ_HANDLER; +void tim15_irq_handler(void) ATTR_IRQ_HANDLER; +void tim16_irq_handler(void) ATTR_IRQ_HANDLER; +void tim17_irq_handler(void) ATTR_IRQ_HANDLER; void exti0_irq_handler(void) ATTR_IRQ_HANDLER; void exti1_irq_handler(void) ATTR_IRQ_HANDLER; void exti2_irq_handler(void) ATTR_IRQ_HANDLER; diff --git a/firmware/target/arm/stm32/timer-stm32h7.c b/firmware/target/arm/stm32/timer-stm32h7.c index 5135d9a384..a5610bf10c 100644 --- a/firmware/target/arm/stm32/timer-stm32h7.c +++ b/firmware/target/arm/stm32/timer-stm32h7.c @@ -18,19 +18,88 @@ * ****************************************************************************/ #include "timer.h" +#include "nvic-arm.h" +#include "clock-stm32h7.h" +#include "regs/stm32h743/rcc.h" +#include "regs/stm32h743/tim.h" + +static uint32_t tim_reg; +static int tim_irq; +static const struct stm32_clock *tim_clk; + +void stm32h7_timer_init(uint32_t instance, + int irq, + const struct stm32_clock *clock) +{ + tim_reg = instance; + tim_irq = irq; + tim_clk = clock; +} + +void stm32h7_timer_irq(void) +{ + reg_assignlf(tim_reg, TIM_SR, UIF(0)); + + if (pfn_timer) + pfn_timer(); +} bool timer_set(long cycles, bool start) { - (void)cycles; - (void)start; - return false; + if (cycles <= 0) + return false; + + /* Calculate timer interval */ + uint32_t counter = cycles; + uint32_t prescaler = 1; + while (counter > 0xFFFF && prescaler < 0x10000) + { + counter >>= 1; + prescaler <<= 1; + } + + /* Duration too long */ + if (counter >= 0xFFFF) + return false; + + /* Unregister old function */ + if (start && pfn_unregister) + { + pfn_unregister(); + pfn_unregister = NULL; + } + + /* Set up timer */ + stm32_clock_enable(tim_clk); + reg_writef(RCC_APB1LENR, TIM7EN(1)); + reg_assignlf(tim_reg, TIM_CR1, URS(1)); + reg_assignlf(tim_reg, TIM_EGR, UG(1)); + reg_assignlf(tim_reg, TIM_DIER, UIE(1)); + reg_assignlf(tim_reg, TIM_SR, UIF(1)); + reg_varl(tim_reg, TIM_CNT) = 0; + reg_varl(tim_reg, TIM_ARR) = counter; + reg_varl(tim_reg, TIM_PSC) = prescaler - 1; + + if (start) + return timer_start(); + + return true; } bool timer_start(void) { - return false; + stm32_clock_enable(tim_clk); + nvic_enable_irq(tim_irq); + + reg_writef(RCC_APB1LENR, TIM7EN(1)); + reg_writelf(tim_reg, TIM_CR1, URS(0), CEN(1)); + return true; } void timer_stop(void) { + nvic_disable_irq(tim_irq); + + reg_writelf(tim_reg, TIM_CR1, CEN(0)); + stm32_clock_disable(tim_clk); } diff --git a/firmware/target/arm/stm32/timer-stm32h7.h b/firmware/target/arm/stm32/timer-stm32h7.h new file mode 100644 index 0000000000..4b7527b73c --- /dev/null +++ b/firmware/target/arm/stm32/timer-stm32h7.h @@ -0,0 +1,31 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 Aidan MacDonald + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef __TIMER_STM32H7_H__ +#define __TIMER_STM32H7_H__ + +#include + +struct stm32_clock; + +void stm32h7_timer_init(uint32_t instance, int irq, + const struct stm32_clock *clock); +void stm32h7_timer_irq(void); + +#endif /* __TIMER_STM32H7_H__ */ diff --git a/firmware/target/arm/stm32/vectors-stm32h7.S b/firmware/target/arm/stm32/vectors-stm32h7.S index d283d808ad..9ee0b153f6 100644 --- a/firmware/target/arm/stm32/vectors-stm32h7.S +++ b/firmware/target/arm/stm32/vectors-stm32h7.S @@ -50,13 +50,13 @@ __vectors_platform: .word UIE /* [ 21] */ .word UIE /* [ 22] */ .word exti9_5_irq_handler /* [ 23] EXTI5 to EXTI9 */ - .word UIE /* [ 24] */ - .word UIE /* [ 25] */ - .word UIE /* [ 26] */ - .word UIE /* [ 27] */ - .word UIE /* [ 28] */ - .word UIE /* [ 29] */ - .word UIE /* [ 30] */ + .word tim1_brk_irq_handler /* [ 24] TIM1_BRK */ + .word tim1_up_irq_handler /* [ 25] TIM1_UP */ + .word tim1_trg_irq_handler /* [ 26] TIM1_TRG_COM */ + .word tim1_cc_irq_handler /* [ 27] TIM1_CC */ + .word tim2_irq_handler /* [ 28] TIM2 */ + .word tim3_irq_handler /* [ 29] TIM3 */ + .word tim4_irq_handler /* [ 30] TIM4 */ .word i2c1_irq_handler /* [ 31] I2C1 event */ .word i2c1_irq_handler /* [ 32] I2C1 error */ .word i2c2_irq_handler /* [ 33] I2C2 event */ @@ -69,10 +69,10 @@ __vectors_platform: .word exti15_10_irq_handler /* [ 40] EXTI10 to EXTI15 */ .word UIE /* [ 41] */ .word UIE /* [ 42] */ - .word UIE /* [ 43] */ - .word UIE /* [ 44] */ - .word UIE /* [ 45] */ - .word UIE /* [ 46] */ + .word tim8_brk_tim12_irq_handler /* [ 43] TIM8_BRK / TIM12 */ + .word tim8_up_tim13_irq_handler /* [ 44] TIM8_UP / TIM13 */ + .word tim8_trg_tim14_irq_handler /* [ 45] TIM8_TRG_COM / TIM14 */ + .word tim8_cc_irq_handler /* [ 46] TIM8_CC */ .word dma1_ch7_irq_handler /* [ 47] DMA1 Stream7 */ .word UIE /* [ 48] */ .word sdmmc1_irq_handler /* [ 49] SDMMC1 */ @@ -80,8 +80,8 @@ __vectors_platform: .word spi3_irq_handler /* [ 51] SPI3 */ .word UIE /* [ 52] */ .word UIE /* [ 53] */ - .word UIE /* [ 54] */ - .word UIE /* [ 55] */ + .word tim6_dac_irq_handler /* [ 54] TIM6 / DAC */ + .word tim7_irq_handler /* [ 55] TIM7 */ .word dma2_ch0_irq_handler /* [ 56] DMA2 Stream0 */ .word dma2_ch1_irq_handler /* [ 57] DMA2 Stream1 */ .word dma2_ch2_irq_handler /* [ 58] DMA2 Stream2 */ @@ -142,9 +142,9 @@ __vectors_platform: .word UIE /* [113] */ .word sai3_irq_handler /* [114] SAI3 */ .word UIE /* [115] */ - .word UIE /* [116] */ - .word UIE /* [117] */ - .word UIE /* [118] */ + .word tim15_irq_handler /* [116] TIM15 */ + .word tim16_irq_handler /* [117] TIM16 */ + .word tim17_irq_handler /* [118] TIM17 */ .word UIE /* [119] */ .word UIE /* [120] */ .word UIE /* [121] */ From 7e6ae1e0d81206f7971c677cabd78e0cd3502aa4 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Sun, 3 May 2026 00:49:18 +0100 Subject: [PATCH 007/408] echoplayer: enable plugins First pass at enabling plugins and setting up keymaps. Change-Id: I3140160bcc3044850416904840b35065f86ac32d --- apps/plugins/battery_bench.c | 6 ++++++ apps/plugins/blackjack.c | 16 ++++++++++++++++ apps/plugins/brickmania.c | 8 ++++++++ apps/plugins/calculator.c | 11 +++++++++++ apps/plugins/calendar.c | 10 ++++++++++ apps/plugins/chessbox/chessbox_pgn.h | 14 ++++++++++++++ apps/plugins/chessclock.c | 10 ++++++++++ apps/plugins/chip8.c | 13 +++++++++++++ apps/plugins/chopper.c | 6 ++++++ apps/plugins/clix.c | 8 ++++++++ apps/plugins/cube.c | 10 ++++++++++ apps/plugins/doom/i_video.c | 12 ++++++++++++ apps/plugins/flipit.c | 12 ++++++++++++ apps/plugins/fractals/fractal.h | 12 ++++++++++++ apps/plugins/goban/goban.h | 10 ++++++++++ apps/plugins/imageviewer/imageviewer_button.h | 12 ++++++++++++ apps/plugins/invadrox.c | 7 +++++++ apps/plugins/jewels.c | 10 ++++++++++ apps/plugins/lib/keymaps.h | 8 ++++++++ apps/plugins/lib/pluginlib_actions.c | 15 +++++++++++++++ apps/plugins/midi/midiplay.c | 8 ++++++++ apps/plugins/minesweeper.c | 10 ++++++++++ apps/plugins/mp3_encoder.c | 6 ++++++ apps/plugins/mpegplayer/mpeg_settings.c | 8 ++++++++ apps/plugins/mpegplayer/mpegplayer.c | 9 +++++++++ apps/plugins/oscilloscope.c | 11 +++++++++++ apps/plugins/pacbox/pacbox.h | 13 ++++++++++++- apps/plugins/pegbox.c | 17 +++++++++++++++++ apps/plugins/pong.c | 8 ++++++++ apps/plugins/reversi/reversi-gui.h | 9 +++++++++ apps/plugins/rockblox.c | 13 +++++++++++++ apps/plugins/rockboy/rockboy.c | 12 ++++++++++++ apps/plugins/rockpaint.c | 11 +++++++++++ apps/plugins/sliding_puzzle.c | 9 +++++++++ apps/plugins/snake.c | 8 ++++++++ apps/plugins/snake2.c | 9 +++++++++ apps/plugins/sokoban.c | 13 +++++++++++++ apps/plugins/solitaire.c | 18 ++++++++++++++++++ apps/plugins/spacerocks.c | 10 ++++++++++ apps/plugins/star.c | 17 +++++++++++++++++ apps/plugins/stopwatch.c | 8 ++++++++ apps/plugins/sudoku/sudoku.h | 10 ++++++++++ apps/plugins/text_viewer/tv_button.h | 10 ++++++++++ apps/plugins/vu_meter.c | 11 +++++++++++ apps/plugins/wormlet.c | 10 ++++++++++ apps/plugins/xobox.c | 9 +++++++++ apps/plugins/zxbox/keymaps.h | 9 +++++++++ apps/plugins/zxbox/zxbox_keyb.c | 9 +++++++++ tools/configure | 2 +- 49 files changed, 505 insertions(+), 2 deletions(-) diff --git a/apps/plugins/battery_bench.c b/apps/plugins/battery_bench.c index 284043c14f..ec7ad45bb8 100644 --- a/apps/plugins/battery_bench.c +++ b/apps/plugins/battery_bench.c @@ -264,6 +264,12 @@ struct battery_tables_t { #define BATTERY_ON_TXT "A - start" #define BATTERY_OFF_TXT "B" +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define BATTERY_ON BUTTON_START +#define BATTERY_OFF BUTTON_SELECT +#define BATTERY_ON_TXT "Start" +#define BATTERY_OFF_TXT "Select" + #else #error "No keymap defined!" #endif diff --git a/apps/plugins/blackjack.c b/apps/plugins/blackjack.c index d5a84b7e4e..cca73029b2 100644 --- a/apps/plugins/blackjack.c +++ b/apps/plugins/blackjack.c @@ -627,6 +627,22 @@ enum { #define BJACK_RIGHT BUTTON_RIGHT #define BJACK_LEFT BUTTON_LEFT +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define BJACK_SELECT_NAME "A" +#define BJACK_STAY_NAME "X" +#define BJACK_QUIT_NAME "B" +#define BJACK_DOUBLE_NAME "Y" +#define BJACK_SELECT BUTTON_A +#define BJACK_QUIT BUTTON_POWER +#define BJACK_MAX BUTTON_B +#define BJACK_MIN BUTTON_Y +#define BJACK_STAY BUTTON_X +#define BJACK_DOUBLEDOWN BUTTON_START +#define BJACK_UP BUTTON_UP +#define BJACK_DOWN BUTTON_DOWN +#define BJACK_RIGHT BUTTON_RIGHT +#define BJACK_LEFT BUTTON_LEFT + #else #error No keymap defined! #endif diff --git a/apps/plugins/brickmania.c b/apps/plugins/brickmania.c index 855aeb98db..4ec3a40f3a 100644 --- a/apps/plugins/brickmania.c +++ b/apps/plugins/brickmania.c @@ -381,6 +381,14 @@ CONFIG_KEYPAD == SANSA_M200_PAD #define UP BUTTON_UP #define DOWN BUTTON_DOWN +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define QUIT BUTTON_POWER +#define LEFT BUTTON_LEFT +#define RIGHT BUTTON_RIGHT +#define SELECT BUTTON_A +#define UP BUTTON_UP +#define DOWN BUTTON_DOWN + #else #error No keymap defined! #endif diff --git a/apps/plugins/calculator.c b/apps/plugins/calculator.c index 2feee39799..0b06609573 100644 --- a/apps/plugins/calculator.c +++ b/apps/plugins/calculator.c @@ -557,6 +557,17 @@ F3: equal to "=" #define CALCULATOR_CALC BUTTON_MENU #define CALCULATOR_CLEAR BUTTON_USER +#elif CONFIG_KEYPAD == ECHO_R1_PAD + +#define CALCULATOR_LEFT BUTTON_LEFT +#define CALCULATOR_RIGHT BUTTON_RIGHT +#define CALCULATOR_UP BUTTON_UP +#define CALCULATOR_DOWN BUTTON_DOWN +#define CALCULATOR_QUIT BUTTON_POWER +#define CALCULATOR_INPUT BUTTON_A +#define CALCULATOR_CALC BUTTON_B +#define CALCULATOR_CLEAR BUTTON_X + #else #error No keymap defined! #endif diff --git a/apps/plugins/calendar.c b/apps/plugins/calendar.c index f605c3267c..b1c2662038 100644 --- a/apps/plugins/calendar.c +++ b/apps/plugins/calendar.c @@ -442,6 +442,16 @@ #define CALENDAR_NEXT_MONTH BUTTON_POWER #define CALENDAR_PREV_MONTH BUTTON_USER +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define CALENDAR_QUIT BUTTON_POWER +#define CALENDAR_SELECT BUTTON_A +#define CALENDAR_NEXT_WEEK BUTTON_DOWN +#define CALENDAR_PREV_WEEK BUTTON_UP +#define CALENDAR_NEXT_DAY BUTTON_RIGHT +#define CALENDAR_PREV_DAY BUTTON_LEFT +#define CALENDAR_NEXT_MONTH BUTTON_B +#define CALENDAR_PREV_MONTH BUTTON_Y + #else #error "No keypad setting." #endif diff --git a/apps/plugins/chessbox/chessbox_pgn.h b/apps/plugins/chessbox/chessbox_pgn.h index 8d621dcfb7..eecb70d1c0 100644 --- a/apps/plugins/chessbox/chessbox_pgn.h +++ b/apps/plugins/chessbox/chessbox_pgn.h @@ -613,6 +613,20 @@ #define CB_SCROLL_LEFT (BUTTON_LEFT|BUTTON_REPEAT) #define CB_SCROLL_RIGHT (BUTTON_RIGHT|BUTTON_REPEAT) +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define CB_SELECT BUTTON_A +#define CB_UP BUTTON_UP +#define CB_DOWN BUTTON_DOWN +#define CB_LEFT BUTTON_LEFT +#define CB_RIGHT BUTTON_RIGHT +#define CB_PLAY BUTTON_START +#define CB_LEVEL BUTTON_SELECT +#define CB_MENU BUTTON_POWER +#define CB_SCROLL_UP (BUTTON_UP|BUTTON_REPEAT) +#define CB_SCROLL_DOWN (BUTTON_DOWN|BUTTON_REPEAT) +#define CB_SCROLL_LEFT (BUTTON_LEFT|BUTTON_REPEAT) +#define CB_SCROLL_RIGHT (BUTTON_RIGHT|BUTTON_REPEAT) + #else #error No keymap defined! #endif diff --git a/apps/plugins/chessclock.c b/apps/plugins/chessclock.c index 9de3a78229..5d4ca183ae 100644 --- a/apps/plugins/chessclock.c +++ b/apps/plugins/chessclock.c @@ -417,6 +417,16 @@ #define CHC_SETTINGS_OK BUTTON_SELECT #define CHC_SETTINGS_CANCEL BUTTON_BACK +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define CHC_QUIT BUTTON_POWER +#define CHC_STARTSTOP BUTTON_START +#define CHC_RESET BUTTON_SELECT +#define CHC_MENU BUTTON_X +#define CHC_SETTINGS_INC BUTTON_UP +#define CHC_SETTINGS_DEC BUTTON_DOWN +#define CHC_SETTINGS_OK BUTTON_A +#define CHC_SETTINGS_CANCEL BUTTON_B + #else #error No keymap defined! #endif diff --git a/apps/plugins/chip8.c b/apps/plugins/chip8.c index d791d8408b..0df928eb77 100644 --- a/apps/plugins/chip8.c +++ b/apps/plugins/chip8.c @@ -1313,6 +1313,19 @@ CONFIG_KEYPAD == MROBE500_PAD #define CHIP8_KEY8 BUTTON_POWER #define CHIP8_KEY9 BUTTON_USER +#elif (CONFIG_KEYPAD == ECHO_R1_PAD) +#define CHIP8_OFF BUTTON_POWER +#define CHIP8_KEY1 BUTTON_Y +#define CHIP8_KEY2 BUTTON_UP +#define CHIP8_KEY3 BUTTON_B +#define CHIP8_KEY4 BUTTON_LEFT +#define CHIP8_KEY5 BUTTON_A +#define CHIP8_KEY6 BUTTON_RIGHT +#define CHIP8_KEY7 BUTTON_SELECT +#define CHIP8_KEY8 BUTTON_DOWN +#define CHIP8_KEY9 BUTTON_START +#define CHIP8_KEYA BUTTON_X + #elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD /* use touchscreen */ diff --git a/apps/plugins/chopper.c b/apps/plugins/chopper.c index f31e834b4c..8dfa68a2b8 100644 --- a/apps/plugins/chopper.c +++ b/apps/plugins/chopper.c @@ -212,6 +212,12 @@ CONFIG_KEYPAD == MROBE500_PAD #define ACTION2 BUTTON_MENU #define ACTIONTEXT "A" +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define QUIT BUTTON_POWER +#define ACTION BUTTON_A +#define ACTION2 BUTTON_UP +#define ACTIONTEXT "A" + #elif !defined(HAVE_TOUCHSCREEN) #error No keymap defined! #endif diff --git a/apps/plugins/clix.c b/apps/plugins/clix.c index b8e8d2164b..721f2b85fa 100644 --- a/apps/plugins/clix.c +++ b/apps/plugins/clix.c @@ -332,6 +332,14 @@ #define CLIX_BUTTON_UP BUTTON_UP #define CLIX_BUTTON_DOWN BUTTON_DOWN +#elif (CONFIG_KEYPAD == ECHO_R1_PAD) +#define CLIX_BUTTON_QUIT BUTTON_POWER +#define CLIX_BUTTON_LEFT BUTTON_LEFT +#define CLIX_BUTTON_RIGHT BUTTON_RIGHT +#define CLIX_BUTTON_CLICK BUTTON_A +#define CLIX_BUTTON_UP BUTTON_UP +#define CLIX_BUTTON_DOWN BUTTON_DOWN + #else #error "no keymap" #endif diff --git a/apps/plugins/cube.c b/apps/plugins/cube.c index 5ea60363c5..fec40eb274 100644 --- a/apps/plugins/cube.c +++ b/apps/plugins/cube.c @@ -424,6 +424,16 @@ #define CUBE_PAUSE BUTTON_USER #define CUBE_HIGHSPEED BUTTON_SELECT +#elif (CONFIG_KEYPAD == ECHO_R1_PAD) +#define CUBE_QUIT BUTTON_POWER +#define CUBE_NEXT BUTTON_RIGHT +#define CUBE_PREV BUTTON_LEFT +#define CUBE_INC BUTTON_UP +#define CUBE_DEC BUTTON_DOWN +#define CUBE_MODE BUTTON_SELECT +#define CUBE_PAUSE BUTTON_START +#define CUBE_HIGHSPEED BUTTON_A + #else #error No keymap defined! #endif diff --git a/apps/plugins/doom/i_video.c b/apps/plugins/doom/i_video.c index a66bf25852..1e64c2f40d 100644 --- a/apps/plugins/doom/i_video.c +++ b/apps/plugins/doom/i_video.c @@ -643,6 +643,18 @@ void I_ShutdownGraphics(void) #define DOOMBUTTON_ENTER BUTTON_POWER #define DOOMBUTTON_WEAPON BUTTON_USER +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define DOOMBUTTON_UP BUTTON_UP +#define DOOMBUTTON_DOWN BUTTON_DOWN +#define DOOMBUTTON_LEFT BUTTON_LEFT +#define DOOMBUTTON_RIGHT BUTTON_RIGHT +#define DOOMBUTTON_SHOOT BUTTON_A +#define DOOMBUTTON_OPEN BUTTON_B +#define DOOMBUTTON_ESC BUTTON_POWER +#define DOOMBUTTON_ENTER BUTTON_X +#define DOOMBUTTON_WEAPON BUTTON_Y +#define DOOMBUTTON_MAP BUTTON_START + #else #error Keymap not defined! #endif diff --git a/apps/plugins/flipit.c b/apps/plugins/flipit.c index 42a1e4b9af..66bf87cc94 100644 --- a/apps/plugins/flipit.c +++ b/apps/plugins/flipit.c @@ -522,6 +522,18 @@ #define FLIPIT_STEP_BY_STEP BUTTON_POWER #define FLIPIT_TOGGLE BUTTON_SELECT +#elif CONFIG_KEYPAD == ECHO_R1_PAD + +#define FLIPIT_LEFT BUTTON_LEFT +#define FLIPIT_RIGHT BUTTON_RIGHT +#define FLIPIT_UP BUTTON_UP +#define FLIPIT_DOWN BUTTON_DOWN +#define FLIPIT_QUIT BUTTON_POWER +#define FLIPIT_SHUFFLE BUTTON_SELECT +#define FLIPIT_SOLVE BUTTON_START +#define FLIPIT_STEP_BY_STEP BUTTON_X +#define FLIPIT_TOGGLE BUTTON_A + #else #error No keymap defined! #endif diff --git a/apps/plugins/fractals/fractal.h b/apps/plugins/fractals/fractal.h index 79324a507b..96a41736e7 100644 --- a/apps/plugins/fractals/fractal.h +++ b/apps/plugins/fractals/fractal.h @@ -530,6 +530,18 @@ #define FRACTAL_PRECISION_DEC (BUTTON_USER|BUTTON_REPEAT) #define FRACTAL_RESET BUTTON_POWER +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define FRACTAL_QUIT BUTTON_POWER +#define FRACTAL_UP BUTTON_UP +#define FRACTAL_DOWN BUTTON_DOWN +#define FRACTAL_LEFT BUTTON_LEFT +#define FRACTAL_RIGHT BUTTON_RIGHT +#define FRACTAL_ZOOM_IN BUTTON_A +#define FRACTAL_ZOOM_OUT BUTTON_B +#define FRACTAL_PRECISION_INC BUTTON_Y +#define FRACTAL_PRECISION_DEC BUTTON_X +#define FRACTAL_RESET BUTTON_START + #else #error No keymap defined! #endif diff --git a/apps/plugins/goban/goban.h b/apps/plugins/goban/goban.h index 80d69cb9e1..4ecabcca76 100644 --- a/apps/plugins/goban/goban.h +++ b/apps/plugins/goban/goban.h @@ -519,6 +519,16 @@ #define GBN_BUTTON_PLAY BUTTON_SELECT #define GBN_BUTTON_MENU BUTTON_MENU +#elif (CONFIG_KEYPAD == ECHO_R1_PAD) +#define GBN_BUTTON_UP BUTTON_UP +#define GBN_BUTTON_DOWN BUTTON_DOWN +#define GBN_BUTTON_LEFT BUTTON_LEFT +#define GBN_BUTTON_RIGHT BUTTON_RIGHT +#define GBN_BUTTON_RETREAT BUTTON_X +#define GBN_BUTTON_ADVANCE BUTTON_Y +#define GBN_BUTTON_PLAY BUTTON_A +#define GBN_BUTTON_MENU BUTTON_POWER + #else #error Unsupported keypad #endif diff --git a/apps/plugins/imageviewer/imageviewer_button.h b/apps/plugins/imageviewer/imageviewer_button.h index d86f4c812e..2396c647b3 100644 --- a/apps/plugins/imageviewer/imageviewer_button.h +++ b/apps/plugins/imageviewer/imageviewer_button.h @@ -571,6 +571,18 @@ #define IMGVIEW_MENU BUTTON_MENU #define IMGVIEW_QUIT BUTTON_BACK +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define IMGVIEW_ZOOM_IN BUTTON_A +#define IMGVIEW_ZOOM_OUT BUTTON_B +#define IMGVIEW_UP BUTTON_UP +#define IMGVIEW_DOWN BUTTON_DOWN +#define IMGVIEW_LEFT BUTTON_LEFT +#define IMGVIEW_RIGHT BUTTON_RIGHT +#define IMGVIEW_NEXT BUTTON_Y +#define IMGVIEW_PREVIOUS BUTTON_X +#define IMGVIEW_MENU BUTTON_START +#define IMGVIEW_QUIT BUTTON_POWER + #else #error No keymap defined! #endif diff --git a/apps/plugins/invadrox.c b/apps/plugins/invadrox.c index f365e04534..8500e2b558 100644 --- a/apps/plugins/invadrox.c +++ b/apps/plugins/invadrox.c @@ -304,6 +304,13 @@ CONFIG_KEYPAD == MROBE500_PAD #define RIGHT BUTTON_RIGHT #define FIRE BUTTON_SELECT +#elif CONFIG_KEYPAD == ECHO_R1_PAD + +#define QUIT BUTTON_POWER +#define LEFT BUTTON_LEFT +#define RIGHT BUTTON_RIGHT +#define FIRE BUTTON_A + #else #error INVADROX: Unsupported keypad #endif diff --git a/apps/plugins/jewels.c b/apps/plugins/jewels.c index 0cf4fd1743..779febed22 100644 --- a/apps/plugins/jewels.c +++ b/apps/plugins/jewels.c @@ -399,6 +399,16 @@ CONFIG_KEYPAD == MROBE500_PAD #define HK_SELECT "A" #define HK_CANCEL "B" +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define JEWELS_UP BUTTON_UP +#define JEWELS_DOWN BUTTON_DOWN +#define JEWELS_LEFT BUTTON_LEFT +#define JEWELS_RIGHT BUTTON_RIGHT +#define JEWELS_SELECT BUTTON_A +#define JEWELS_CANCEL BUTTON_POWER +#define HK_SELECT "A" +#define HK_CANCEL "POWER" + #else #error No keymap defined! #endif diff --git a/apps/plugins/lib/keymaps.h b/apps/plugins/lib/keymaps.h index 3cd281bf2e..71d5b6ad5f 100644 --- a/apps/plugins/lib/keymaps.h +++ b/apps/plugins/lib/keymaps.h @@ -282,6 +282,14 @@ #define BTN_FIRE BUTTON_USER #define BTN_PAUSE BUTTON_MENU +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define BTN_UP BUTTON_UP +#define BTN_DOWN BUTTON_DOWN +#define BTN_LEFT BUTTON_LEFT +#define BTN_RIGHT BUTTON_RIGHT +#define BTN_FIRE BUTTON_A +#define BTN_PAUSE BUTTON_POWER + #else #error Unsupported keypad #endif diff --git a/apps/plugins/lib/pluginlib_actions.c b/apps/plugins/lib/pluginlib_actions.c index 5d2c0e323a..5b175a66a4 100644 --- a/apps/plugins/lib/pluginlib_actions.c +++ b/apps/plugins/lib/pluginlib_actions.c @@ -293,6 +293,15 @@ const struct button_mapping pla_main_ctx[] = { PLA_DOWN_REPEAT, BUTTON_DOWN|BUTTON_REPEAT, BUTTON_NONE }, { PLA_LEFT_REPEAT, BUTTON_LEFT|BUTTON_REPEAT, BUTTON_NONE }, { PLA_RIGHT_REPEAT, BUTTON_RIGHT|BUTTON_REPEAT, BUTTON_NONE }, +#elif CONFIG_KEYPAD == ECHO_R1_PAD + { PLA_UP, BUTTON_UP, BUTTON_NONE }, + { PLA_DOWN, BUTTON_DOWN, BUTTON_NONE }, + { PLA_LEFT, BUTTON_LEFT, BUTTON_NONE }, + { PLA_RIGHT, BUTTON_RIGHT, BUTTON_NONE }, + { PLA_UP_REPEAT, BUTTON_UP|BUTTON_REPEAT, BUTTON_NONE }, + { PLA_DOWN_REPEAT, BUTTON_DOWN|BUTTON_REPEAT, BUTTON_NONE }, + { PLA_LEFT_REPEAT, BUTTON_LEFT|BUTTON_REPEAT, BUTTON_NONE }, + { PLA_RIGHT_REPEAT, BUTTON_RIGHT|BUTTON_REPEAT, BUTTON_NONE }, #else # ifndef HAVE_TOUCHSCREEN # error pluginlib_actions: No directions defined @@ -540,6 +549,12 @@ const struct button_mapping pla_main_ctx[] = {PLA_SELECT, BUTTON_SELECT, BUTTON_NONE}, {PLA_SELECT_REL, BUTTON_SELECT|BUTTON_REL, BUTTON_SELECT}, {PLA_SELECT_REPEAT, BUTTON_SELECT|BUTTON_REPEAT, BUTTON_NONE}, +#elif (CONFIG_KEYPAD == ECHO_R1_PAD) + {PLA_CANCEL, BUTTON_B, BUTTON_NONE}, + {PLA_EXIT, BUTTON_POWER, BUTTON_NONE}, + {PLA_SELECT, BUTTON_A, BUTTON_NONE}, + {PLA_SELECT_REL, BUTTON_A|BUTTON_REL, BUTTON_SELECT}, + {PLA_SELECT_REPEAT, BUTTON_A|BUTTON_REPEAT, BUTTON_NONE}, #else # ifndef HAVE_TOUCHSCREEN # error pluginlib_actions: No actions defined diff --git a/apps/plugins/midi/midiplay.c b/apps/plugins/midi/midiplay.c index 7f2c8ba2c6..5ec6e7ede4 100644 --- a/apps/plugins/midi/midiplay.c +++ b/apps/plugins/midi/midiplay.c @@ -343,6 +343,14 @@ #define MIDI_VOL_DOWN BUTTON_DOWN #define MIDI_PLAYPAUSE BUTTON_USER +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define MIDI_QUIT BUTTON_POWER +#define MIDI_FFWD BUTTON_RIGHT +#define MIDI_REWIND BUTTON_LEFT +#define MIDI_VOL_UP BUTTON_UP +#define MIDI_VOL_DOWN BUTTON_DOWN +#define MIDI_PLAYPAUSE BUTTON_A + #else #error No keymap defined! #endif diff --git a/apps/plugins/minesweeper.c b/apps/plugins/minesweeper.c index 0faff83f33..41145c90b8 100644 --- a/apps/plugins/minesweeper.c +++ b/apps/plugins/minesweeper.c @@ -464,6 +464,16 @@ CONFIG_KEYPAD == MROBE500_PAD # define MINESWP_DISCOVER BUTTON_SELECT # define MINESWP_INFO BUTTON_MENU +#elif CONFIG_KEYPAD == ECHO_R1_PAD +# define MINESWP_LEFT BUTTON_LEFT +# define MINESWP_RIGHT BUTTON_RIGHT +# define MINESWP_UP BUTTON_UP +# define MINESWP_DOWN BUTTON_DOWN +# define MINESWP_QUIT BUTTON_POWER +# define MINESWP_TOGGLE BUTTON_B +# define MINESWP_DISCOVER BUTTON_A +# define MINESWP_INFO BUTTON_START + #else #error No keymap defined! #endif diff --git a/apps/plugins/mp3_encoder.c b/apps/plugins/mp3_encoder.c index b5790f3ae2..af998487e9 100644 --- a/apps/plugins/mp3_encoder.c +++ b/apps/plugins/mp3_encoder.c @@ -2599,6 +2599,12 @@ CONFIG_KEYPAD == MROBE500_PAD #define MP3ENC_DONE BUTTON_POWER #define MP3ENC_SELECT BUTTON_SELECT +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define MP3ENC_PREV BUTTON_UP +#define MP3ENC_NEXT BUTTON_DOWN +#define MP3ENC_DONE BUTTON_POWER +#define MP3ENC_SELECT BUTTON_RIGHT + #else #error No keymap defined! #endif diff --git a/apps/plugins/mpegplayer/mpeg_settings.c b/apps/plugins/mpegplayer/mpeg_settings.c index 3dc621d2e9..3c6ec54a0e 100644 --- a/apps/plugins/mpegplayer/mpeg_settings.c +++ b/apps/plugins/mpegplayer/mpeg_settings.c @@ -373,6 +373,14 @@ struct mpeg_settings settings; #define MPEG_START_TIME_DOWN BUTTON_DOWN #define MPEG_START_TIME_EXIT BUTTON_BACK +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define MPEG_START_TIME_SELECT BUTTON_A +#define MPEG_START_TIME_LEFT BUTTON_LEFT +#define MPEG_START_TIME_RIGHT BUTTON_RIGHT +#define MPEG_START_TIME_UP BUTTON_UP +#define MPEG_START_TIME_DOWN BUTTON_DOWN +#define MPEG_START_TIME_EXIT BUTTON_B + #else #error No keymap defined! #endif diff --git a/apps/plugins/mpegplayer/mpegplayer.c b/apps/plugins/mpegplayer/mpegplayer.c index fcbd872125..60522cd56c 100644 --- a/apps/plugins/mpegplayer/mpegplayer.c +++ b/apps/plugins/mpegplayer/mpegplayer.c @@ -506,6 +506,15 @@ CONFIG_KEYPAD == SANSA_M200_PAD #define MPEG_RW BUTTON_LEFT #define MPEG_FF BUTTON_RIGHT +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define MPEG_MENU BUTTON_UP +#define MPEG_PAUSE BUTTON_DOWN +#define MPEG_STOP BUTTON_POWER +#define MPEG_VOLDOWN BUTTON_VOL_DOWN +#define MPEG_VOLUP BUTTON_VOL_UP +#define MPEG_RW BUTTON_LEFT +#define MPEG_FF BUTTON_RIGHT + #else #error No keymap defined! #endif diff --git a/apps/plugins/oscilloscope.c b/apps/plugins/oscilloscope.c index 8888bdc356..4754416ab7 100644 --- a/apps/plugins/oscilloscope.c +++ b/apps/plugins/oscilloscope.c @@ -575,6 +575,17 @@ #define OSCILLOSCOPE_VOL_UP BUTTON_UP #define OSCILLOSCOPE_VOL_DOWN BUTTON_DOWN +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define OSCILLOSCOPE_QUIT BUTTON_POWER +#define OSCILLOSCOPE_DRAWMODE BUTTON_SELECT +#define OSCILLOSCOPE_ADVMODE BUTTON_START +#define OSCILLOSCOPE_ORIENTATION BUTTON_B +#define OSCILLOSCOPE_PAUSE BUTTON_A +#define OSCILLOSCOPE_SPEED_UP BUTTON_RIGHT +#define OSCILLOSCOPE_SPEED_DOWN BUTTON_LEFT +#define OSCILLOSCOPE_VOL_UP BUTTON_UP +#define OSCILLOSCOPE_VOL_DOWN BUTTON_DOWN + #else #error No keymap defined! #endif diff --git a/apps/plugins/pacbox/pacbox.h b/apps/plugins/pacbox/pacbox.h index e7cef86453..4292e47f18 100644 --- a/apps/plugins/pacbox/pacbox.h +++ b/apps/plugins/pacbox/pacbox.h @@ -428,6 +428,17 @@ #define PACMAN_COIN BUTTON_USER #define PACMAN_MENU BUTTON_MENU +#elif CONFIG_KEYPAD == ECHO_R1_PAD + +#define PACMAN_UP BUTTON_RIGHT +#define PACMAN_DOWN BUTTON_LEFT +#define PACMAN_LEFT BUTTON_UP +#define PACMAN_RIGHT BUTTON_DOWN +#define PACMAN_1UP BUTTON_SELECT +#define PACMAN_2UP BUTTON_START +#define PACMAN_COIN BUTTON_X +#define PACMAN_MENU BUTTON_VOL_UP + #else #error Keymap not defined! @@ -533,7 +544,7 @@ NOTE: pacbox.c assumes this is an integer divisor of 60 */ #if defined(TOSHIBA_GIGABEAT_S) || defined (TOSHIBA_GIGABEAT_F) || \ - defined(SANSA_FUZEPLUS) + defined(SANSA_FUZEPLUS) || defined(ECHO_R1) /* Gigabeat S,F and Sansa Fuze+ can manage the full framerate (1 in 1 frames) */ #define FPS 60 diff --git a/apps/plugins/pegbox.c b/apps/plugins/pegbox.c index 1074555399..7fbac41335 100644 --- a/apps/plugins/pegbox.c +++ b/apps/plugins/pegbox.c @@ -747,6 +747,23 @@ CONFIG_KEYPAD == MROBE500_PAD #define LVL_DOWN_TEXT "Start" #define SELECT_TEXT "A" +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define PEGBOX_SELECT BUTTON_A +#define PEGBOX_QUIT BUTTON_POWER +#define PEGBOX_RESTART BUTTON_X +#define PEGBOX_LVL_UP BUTTON_START +#define PEGBOX_LVL_DOWN BUTTON_SELECT +#define PEGBOX_UP BUTTON_UP +#define PEGBOX_DOWN BUTTON_DOWN +#define PEGBOX_RIGHT BUTTON_RIGHT +#define PEGBOX_LEFT BUTTON_LEFT + +#define QUIT_TEXT "Power" +#define RESTART_TEXT "X" +#define LVL_UP_TEXT "Start" +#define LVL_DOWN_TEXT "Select" +#define SELECT_TEXT "A" + #else #error "Unsupported keymap!" #endif diff --git a/apps/plugins/pong.c b/apps/plugins/pong.c index e7c5e5671c..a7aa93724f 100644 --- a/apps/plugins/pong.c +++ b/apps/plugins/pong.c @@ -344,6 +344,14 @@ CONFIG_KEYPAD == MROBE500_PAD #define PONG_RIGHT_UP BUTTON_MENU #define PONG_RIGHT_DOWN BUTTON_POWER +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define PONG_QUIT BUTTON_POWER +#define PONG_PAUSE BUTTON_START +#define PONG_LEFT_UP BUTTON_Y +#define PONG_LEFT_DOWN BUTTON_LEFT +#define PONG_RIGHT_UP BUTTON_B +#define PONG_RIGHT_DOWN BUTTON_RIGHT + #else #error No keymap defined! #endif diff --git a/apps/plugins/reversi/reversi-gui.h b/apps/plugins/reversi/reversi-gui.h index b8883c7a50..c73acb2797 100644 --- a/apps/plugins/reversi/reversi-gui.h +++ b/apps/plugins/reversi/reversi-gui.h @@ -381,6 +381,15 @@ #define REVERSI_BUTTON_MAKE_MOVE BUTTON_SELECT #define REVERSI_BUTTON_MENU BUTTON_MENU +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define REVERSI_BUTTON_QUIT BUTTON_POWER +#define REVERSI_BUTTON_UP BUTTON_UP +#define REVERSI_BUTTON_DOWN BUTTON_DOWN +#define REVERSI_BUTTON_LEFT BUTTON_LEFT +#define REVERSI_BUTTON_RIGHT BUTTON_RIGHT +#define REVERSI_BUTTON_MAKE_MOVE BUTTON_A +#define REVERSI_BUTTON_MENU BUTTON_START + #else #error No keymap defined! #endif diff --git a/apps/plugins/rockblox.c b/apps/plugins/rockblox.c index 306568b76a..bdc3795301 100644 --- a/apps/plugins/rockblox.c +++ b/apps/plugins/rockblox.c @@ -494,6 +494,19 @@ #define ROCKBLOX_DROP BUTTON_SELECT #define ROCKBLOX_RESTART BUTTON_USER +#elif CONFIG_KEYPAD == ECHO_R1_PAD + +#define ROCKBLOX_OFF BUTTON_POWER +#define ROCKBLOX_LEFT BUTTON_LEFT +#define ROCKBLOX_RIGHT BUTTON_RIGHT +#define ROCKBLOX_ROTATE_CCW BUTTON_UP +#define ROCKBLOX_ROTATE_CW BUTTON_DOWN +#define ROCKBLOX_ROTATE_CCW2 BUTTON_Y +#define ROCKBLOX_ROTATE_CW2 BUTTON_B +#define ROCKBLOX_DOWN BUTTON_X +#define ROCKBLOX_DROP BUTTON_A +#define ROCKBLOX_RESTART BUTTON_START + #else #error No keymap defined! #endif diff --git a/apps/plugins/rockboy/rockboy.c b/apps/plugins/rockboy/rockboy.c index 16889108b9..96fb2c68ba 100644 --- a/apps/plugins/rockboy/rockboy.c +++ b/apps/plugins/rockboy/rockboy.c @@ -494,6 +494,18 @@ static void setoptions (void) options.SELECT = BUTTON_USER; options.MENU = BUTTON_MENU; +#elif CONFIG_KEYPAD == ECHO_R1_PAD + options.UP = BUTTON_UP; + options.DOWN = BUTTON_DOWN; + options.LEFT = BUTTON_LEFT; + options.RIGHT = BUTTON_RIGHT; + + options.A = BUTTON_A; + options.B = BUTTON_B; + options.START = BUTTON_START; + options.SELECT = BUTTON_SELECT; + options.MENU = BUTTON_POWER; + #else #error No Keymap Defined! #endif diff --git a/apps/plugins/rockpaint.c b/apps/plugins/rockpaint.c index 6fdedb0736..bfde6c53f0 100644 --- a/apps/plugins/rockpaint.c +++ b/apps/plugins/rockpaint.c @@ -439,6 +439,17 @@ #define ROCKPAINT_LEFT BUTTON_LEFT #define ROCKPAINT_RIGHT BUTTON_RIGHT +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define ROCKPAINT_QUIT BUTTON_POWER +#define ROCKPAINT_DRAW BUTTON_A +#define ROCKPAINT_MENU BUTTON_B +#define ROCKPAINT_TOOLBAR BUTTON_X +#define ROCKPAINT_TOOLBAR2 BUTTON_Y +#define ROCKPAINT_UP BUTTON_UP +#define ROCKPAINT_DOWN BUTTON_DOWN +#define ROCKPAINT_LEFT BUTTON_LEFT +#define ROCKPAINT_RIGHT BUTTON_RIGHT + #else #error "Please define keys for this keypad" #endif diff --git a/apps/plugins/sliding_puzzle.c b/apps/plugins/sliding_puzzle.c index d3c223e2ec..d3189945ee 100644 --- a/apps/plugins/sliding_puzzle.c +++ b/apps/plugins/sliding_puzzle.c @@ -377,6 +377,15 @@ CONFIG_KEYPAD == MROBE500_PAD #define PUZZLE_SHUFFLE BUTTON_SELECT #define PUZZLE_PICTURE BUTTON_MENU +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define PUZZLE_QUIT BUTTON_POWER +#define PUZZLE_LEFT BUTTON_LEFT +#define PUZZLE_RIGHT BUTTON_RIGHT +#define PUZZLE_UP BUTTON_UP +#define PUZZLE_DOWN BUTTON_DOWN +#define PUZZLE_SHUFFLE BUTTON_SELECT +#define PUZZLE_PICTURE BUTTON_START + #else #error No keymap defined! #endif diff --git a/apps/plugins/snake.c b/apps/plugins/snake.c index a2213cf0b5..49c4ca81e8 100644 --- a/apps/plugins/snake.c +++ b/apps/plugins/snake.c @@ -331,6 +331,14 @@ dir is the current direction of the snake - 0=up, 1=right, 2=down, 3=left; #define SNAKE_DOWN BUTTON_DOWN #define SNAKE_PLAYPAUSE BUTTON_SELECT +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define SNAKE_QUIT BUTTON_POWER +#define SNAKE_LEFT BUTTON_LEFT +#define SNAKE_RIGHT BUTTON_RIGHT +#define SNAKE_UP BUTTON_UP +#define SNAKE_DOWN BUTTON_DOWN +#define SNAKE_PLAYPAUSE BUTTON_START + #else #error No keymap defined! #endif diff --git a/apps/plugins/snake2.c b/apps/plugins/snake2.c index 9a269b48be..b5f15b80c2 100644 --- a/apps/plugins/snake2.c +++ b/apps/plugins/snake2.c @@ -478,6 +478,15 @@ CONFIG_KEYPAD == MROBE500_PAD #define SNAKE2_PLAYPAUSE BUTTON_SELECT #define SNAKE2_PLAYPAUSE_TEXT "A" +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define SNAKE2_LEFT BUTTON_LEFT +#define SNAKE2_RIGHT BUTTON_RIGHT +#define SNAKE2_UP BUTTON_UP +#define SNAKE2_DOWN BUTTON_DOWN +#define SNAKE2_QUIT BUTTON_POWER +#define SNAKE2_PLAYPAUSE BUTTON_START +#define SNAKE2_PLAYPAUSE_TEXT "START" + #else #error No keymap defined! #endif diff --git a/apps/plugins/sokoban.c b/apps/plugins/sokoban.c index ae5ccf1574..027ad99c7e 100644 --- a/apps/plugins/sokoban.c +++ b/apps/plugins/sokoban.c @@ -733,6 +733,19 @@ #define SOKOBAN_LEVEL_REPEAT_NAME "[TOPRIGHT]" #define BUTTON_SAVE_NAME "A [CENTER]" +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define SOKOBAN_LEFT BUTTON_LEFT +#define SOKOBAN_RIGHT BUTTON_RIGHT +#define SOKOBAN_UP BUTTON_UP +#define SOKOBAN_DOWN BUTTON_DOWN +#define SOKOBAN_MENU BUTTON_POWER +#define SOKOBAN_UNDO BUTTON_X +#define SOKOBAN_REDO BUTTON_Y +#define SOKOBAN_LEVEL_REPEAT BUTTON_SELECT +#define SOKOBAN_PAUSE BUTTON_START +#define BUTTON_SAVE BUTTON_A +#define BUTTON_SAVE_NAME "A" + #else #error No keymap defined! #endif diff --git a/apps/plugins/solitaire.c b/apps/plugins/solitaire.c index 215412164a..aa5f876c80 100644 --- a/apps/plugins/solitaire.c +++ b/apps/plugins/solitaire.c @@ -780,6 +780,24 @@ CONFIG_KEYPAD == MROBE500_PAD # define HK_REM2STACK "MENU+UP" +#elif (CONFIG_KEYPAD == ECHO_R1_PAD) +# define SOL_QUIT BUTTON_POWER +# define SOL_UP BUTTON_UP +# define SOL_DOWN BUTTON_DOWN +# define SOL_LEFT BUTTON_LEFT +# define SOL_RIGHT BUTTON_RIGHT +# define SOL_MOVE_PRE BUTTON_A +# define SOL_MOVE (BUTTON_A | BUTTON_REL) +# define SOL_DRAW BUTTON_X +# define SOL_REM2CUR (BUTTON_Y | BUTTON_REPEAT) +# define SOL_CUR2STACK (BUTTON_A | BUTTON_REPEAT) +# define SOL_REM2STACK BUTTON_START +# define HK_MOVE "A" +# define HK_DRAW "X" +# define HK_REM2CUR "Long Y" +# define HK_CUR2STACK "Long A.." +# define HK_REM2STACK "Start" + #else #error No keymap defined! #endif diff --git a/apps/plugins/spacerocks.c b/apps/plugins/spacerocks.c index 1555c2d4ad..611a373418 100644 --- a/apps/plugins/spacerocks.c +++ b/apps/plugins/spacerocks.c @@ -392,6 +392,16 @@ #define AST_RIGHT BUTTON_RIGHT #define AST_FIRE BUTTON_SELECT +#elif CONFIG_KEYPAD == ECHO_R1_PAD + +#define AST_PAUSE BUTTON_START +#define AST_QUIT BUTTON_POWER +#define AST_THRUST BUTTON_UP +#define AST_HYPERSPACE BUTTON_DOWN +#define AST_LEFT BUTTON_LEFT +#define AST_RIGHT BUTTON_RIGHT +#define AST_FIRE BUTTON_A + #else #error No keymap defined! #endif diff --git a/apps/plugins/star.c b/apps/plugins/star.c index 14c4d671e2..735fb3bc60 100644 --- a/apps/plugins/star.c +++ b/apps/plugins/star.c @@ -704,6 +704,23 @@ #define STAR_LEVEL_DOWN_NAME "Start" #define STAR_LEVEL_REPEAT_NAME "Y" +#elif CONFIG_KEYPAD == ECHO_R1_PAD + +#define STAR_QUIT BUTTON_POWER +#define STAR_LEFT BUTTON_LEFT +#define STAR_RIGHT BUTTON_RIGHT +#define STAR_UP BUTTON_UP +#define STAR_DOWN BUTTON_DOWN +#define STAR_TOGGLE_CONTROL BUTTON_A +#define STAR_LEVEL_UP BUTTON_START +#define STAR_LEVEL_DOWN BUTTON_SELECT +#define STAR_LEVEL_REPEAT BUTTON_X +#define STAR_TOGGLE_CONTROL_NAME "A" +#define STAR_QUIT_NAME "Power" +#define STAR_LEVEL_UP_NAME "Start" +#define STAR_LEVEL_DOWN_NAME "Select" +#define STAR_LEVEL_REPEAT_NAME "X" + #else #error No keymap defined! #endif diff --git a/apps/plugins/stopwatch.c b/apps/plugins/stopwatch.c index c0a6b2dc01..550904efd9 100644 --- a/apps/plugins/stopwatch.c +++ b/apps/plugins/stopwatch.c @@ -313,6 +313,14 @@ #define STOPWATCH_SCROLL_UP BUTTON_UP #define STOPWATCH_SCROLL_DOWN BUTTON_DOWN +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define STOPWATCH_QUIT BUTTON_POWER +#define STOPWATCH_START_STOP BUTTON_START +#define STOPWATCH_RESET_TIMER BUTTON_SELECT +#define STOPWATCH_LAP_TIMER BUTTON_RIGHT +#define STOPWATCH_SCROLL_UP BUTTON_UP +#define STOPWATCH_SCROLL_DOWN BUTTON_DOWN + #else #error No keymap defined! #endif diff --git a/apps/plugins/sudoku/sudoku.h b/apps/plugins/sudoku/sudoku.h index fe2bf6c592..c43a6b0d96 100644 --- a/apps/plugins/sudoku/sudoku.h +++ b/apps/plugins/sudoku/sudoku.h @@ -482,6 +482,16 @@ #define SUDOKU_BUTTON_TOGGLE BUTTON_SELECT #define SUDOKU_BUTTON_POSSIBLE BUTTON_USER +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define SUDOKU_BUTTON_QUIT BUTTON_POWER +#define SUDOKU_BUTTON_UP BUTTON_UP +#define SUDOKU_BUTTON_DOWN BUTTON_DOWN +#define SUDOKU_BUTTON_LEFT BUTTON_LEFT +#define SUDOKU_BUTTON_RIGHT BUTTON_RIGHT +#define SUDOKU_BUTTON_MENU BUTTON_START +#define SUDOKU_BUTTON_TOGGLE BUTTON_A +#define SUDOKU_BUTTON_POSSIBLE BUTTON_B + #else #error No keymap defined! #endif diff --git a/apps/plugins/text_viewer/tv_button.h b/apps/plugins/text_viewer/tv_button.h index 03256fe3fd..d1bacb1f10 100644 --- a/apps/plugins/text_viewer/tv_button.h +++ b/apps/plugins/text_viewer/tv_button.h @@ -597,6 +597,16 @@ #define TV_AUTOSCROLL BUTTON_USER #define TV_BOOKMARK BUTTON_SELECT +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define TV_QUIT BUTTON_POWER +#define TV_SCROLL_UP BUTTON_UP +#define TV_SCROLL_DOWN BUTTON_DOWN +#define TV_SCREEN_LEFT BUTTON_LEFT +#define TV_SCREEN_RIGHT BUTTON_RIGHT +#define TV_MENU BUTTON_START +#define TV_AUTOSCROLL BUTTON_A +#define TV_BOOKMARK BUTTON_B + #else #error No keymap defined! #endif diff --git a/apps/plugins/vu_meter.c b/apps/plugins/vu_meter.c index d18b6a658c..e1178fc691 100644 --- a/apps/plugins/vu_meter.c +++ b/apps/plugins/vu_meter.c @@ -479,6 +479,17 @@ #define LABEL_MENU "X" #define LABEL_VOLUME "Up/Down" +#elif CONFIG_KEYPAD == ECHO_R1_PAD +#define VUMETER_QUIT BUTTON_POWER +#define VUMETER_HELP BUTTON_SELECT +#define VUMETER_MENU BUTTON_START +#define VUMETER_UP BUTTON_UP +#define VUMETER_DOWN BUTTON_DOWN +#define LABEL_HELP "Select" +#define LABEL_QUIT "Power" +#define LABEL_MENU "Start" +#define LABEL_VOLUME "Up/Down" + #else #error No keymap defined! #endif diff --git a/apps/plugins/wormlet.c b/apps/plugins/wormlet.c index 5c30c7eb72..fec98315af 100644 --- a/apps/plugins/wormlet.c +++ b/apps/plugins/wormlet.c @@ -428,6 +428,16 @@ CONFIG_KEYPAD == MROBE500_PAD #define BTN_QUIT BUTTON_BACK #define BTN_STOPRESET BUTTON_MENU +#elif CONFIG_KEYPAD == ECHO_R1_PAD + +#define BTN_DIR_UP BUTTON_UP +#define BTN_DIR_DOWN BUTTON_DOWN +#define BTN_DIR_LEFT BUTTON_LEFT +#define BTN_DIR_RIGHT BUTTON_RIGHT +#define BTN_STARTPAUSE BUTTON_START +#define BTN_QUIT BUTTON_POWER +#define BTN_STOPRESET BUTTON_SELECT + #else #error No keymap defined! #endif diff --git a/apps/plugins/xobox.c b/apps/plugins/xobox.c index 84f8fe8698..445ca77b18 100644 --- a/apps/plugins/xobox.c +++ b/apps/plugins/xobox.c @@ -371,6 +371,15 @@ CONFIG_KEYPAD == MROBE500_PAD #define DOWN BUTTON_DOWN #define PAUSE BUTTON_SELECT +#elif CONFIG_KEYPAD == ECHO_R1_PAD + +#define QUIT BUTTON_POWER +#define LEFT BUTTON_LEFT +#define RIGHT BUTTON_RIGHT +#define UP BUTTON_UP +#define DOWN BUTTON_DOWN +#define PAUSE BUTTON_START + #else #error "No keymap defined!" #endif diff --git a/apps/plugins/zxbox/keymaps.h b/apps/plugins/zxbox/keymaps.h index 0bb7d3e80f..fa7394abfa 100644 --- a/apps/plugins/zxbox/keymaps.h +++ b/apps/plugins/zxbox/keymaps.h @@ -310,6 +310,15 @@ #define ZX_SELECT BUTTON_SELECT #define ZX_MENU BUTTON_MENU +#elif CONFIG_KEYPAD == ECHO_R1_PAD + +#define ZX_UP BUTTON_UP +#define ZX_DOWN BUTTON_DOWN +#define ZX_LEFT BUTTON_LEFT +#define ZX_RIGHT BUTTON_RIGHT +#define ZX_SELECT BUTTON_A +#define ZX_MENU BUTTON_POWER + #else #error Keymap not defined! diff --git a/apps/plugins/zxbox/zxbox_keyb.c b/apps/plugins/zxbox/zxbox_keyb.c index 6a5c2a6c81..053215ce6d 100644 --- a/apps/plugins/zxbox/zxbox_keyb.c +++ b/apps/plugins/zxbox/zxbox_keyb.c @@ -300,6 +300,15 @@ #define KBD_UP BUTTON_UP #define KBD_DOWN BUTTON_DOWN +#elif CONFIG_KEYPAD == ECHO_R1_PAD + +#define KBD_SELECT BUTTON_A +#define KBD_ABORT BUTTON_B +#define KBD_LEFT BUTTON_LEFT +#define KBD_RIGHT BUTTON_RIGHT +#define KBD_UP BUTTON_UP +#define KBD_DOWN BUTTON_DOWN + #endif #ifdef HAVE_TOUCHSCREEN diff --git a/tools/configure b/tools/configure index 6d798820fa..ff19ab75ab 100755 --- a/tools/configure +++ b/tools/configure @@ -4105,7 +4105,7 @@ fi memory=32 armcortexm7cc appextra="recorder:gui" - plugins="no" + plugins="yes" tool="cp " boottool="cp " output="rockbox.echo" From 37943178f014c2e44e9fcd21e5d9592070e30ad7 Mon Sep 17 00:00:00 2001 From: mojyack Date: Sat, 24 Jan 2026 01:03:24 +0900 Subject: [PATCH 008/408] usb: Directly reference the "driver" in the endpoint data structure Results in cleaner code versus effectively cut-n-pasting the driver's completion callbacks into the endpoint structure. Change-Id: I043c46c91796f787dec098a7db043481834950a0 --- firmware/usbstack/usb_core.c | 59 +++++++++++++++++------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/firmware/usbstack/usb_core.c b/firmware/usbstack/usb_core.c index b0b32e85ea..e8a554db24 100644 --- a/firmware/usbstack/usb_core.c +++ b/firmware/usbstack/usb_core.c @@ -245,17 +245,11 @@ struct usb_transfer_completion_event_data int length; }; -typedef void (*completion_handler_t)(int ep, int dir, int status, int length); -typedef bool (*fast_completion_handler_t)(int ep, int dir, int status, int length); -typedef bool (*control_handler_t)(struct usb_ctrlrequest* req, uint8_t* reqdata, size_t reqdata_size); - static struct { - completion_handler_t completion_handler[2]; - fast_completion_handler_t fast_completion_handler[2]; - control_handler_t control_handler[2]; - struct usb_transfer_completion_event_data completion_event[2]; -} ep_data[USB_NUM_ENDPOINTS]; + struct usb_class_driver* driver; + struct usb_transfer_completion_event_data completion_event; +} ep_data[USB_NUM_ENDPOINTS][2]; struct ep_alloc_state { int8_t type[2]; @@ -506,9 +500,9 @@ void usb_core_handle_transfer_completion( return; } - completion_handler_t handler = ep_data[num].completion_handler[dir]; - if(handler != NULL) - handler(num, dir == DIR_IN ? USB_DIR_IN : USB_DIR_OUT, event->status, event->length); + struct usb_class_driver* driver = ep_data[num][dir].driver; + if(driver && driver->transfer_complete) + driver->transfer_complete(num, dir == DIR_IN ? USB_DIR_IN : USB_DIR_OUT, event->status, event->length); } void usb_core_enable_driver(int driver, bool enabled) @@ -613,9 +607,7 @@ static void init_deinit_endpoints(int config, bool init) { int ep = epnum | (dir == DIR_OUT ? USB_DIR_OUT : USB_DIR_IN); if(init) { usb_drv_ep_init(&cstate->ep_alloc_ctx, ep); - ep_data[epnum].completion_handler[dir] = driver->transfer_complete; - ep_data[epnum].fast_completion_handler[dir] = driver->fast_transfer_complete; - ep_data[epnum].control_handler[dir] = driver->control_request; + ep_data[epnum][dir].driver = driver; } else { usb_drv_ep_deinit(&cstate->ep_alloc_ctx, ep); } @@ -1065,22 +1057,27 @@ static void request_handler_interface(struct usb_ctrlrequest* req, uint8_t* reqd static void request_handler_endpoint_drivers(struct usb_ctrlrequest* req, uint8_t* reqdata, size_t reqdata_size) { - bool handled = false; - control_handler_t control_handler = NULL; + int num = EP_NUM(req->wIndex); + int dir = EP_DIR(req->wIndex); - if(EP_NUM(req->wIndex) < USB_NUM_ENDPOINTS) - control_handler = - ep_data[EP_NUM(req->wIndex)].control_handler[EP_DIR(req->wIndex)]; + if(num >= USB_NUM_ENDPOINTS) + goto error; - if(control_handler) - handled = control_handler(req, reqdata, reqdata_size); + struct usb_class_driver* driver = ep_data[num][dir].driver; - if(!handled) { + if(!driver || !driver->control_request) + goto error; + + if(!driver->control_request(req, reqdata, reqdata_size)) + goto error; + + return; + +error: /* nope. flag error */ logf("bad req 0x%x:0x%x:0x%x:0x%x:0x%x", req->bRequestType,req->bRequest, req->wValue, req->wIndex, req->wLength); usb_core_control_response(USB_CONTROL_STALL, NULL, 0); - } } static void request_handler_endpoint_standard(struct usb_ctrlrequest* req, uint8_t* reqdata, size_t reqdata_size) @@ -1192,7 +1189,7 @@ void usb_core_bus_reset(void) static void signal_xfer_complete(int ep, struct usb_ctrlrequest* req, int status, int length) { struct usb_transfer_completion_event_data* completion_event = - &ep_data[EP_NUM(ep)].completion_event[EP_DIR(ep)]; + &ep_data[EP_NUM(ep)][EP_DIR(ep)].completion_event; completion_event->req = req; completion_event->ep = ep; @@ -1234,24 +1231,24 @@ static bool check_for_new_setup(void) { } /* called by usb_drv_transfer_completed() */ -void usb_core_transfer_complete(int endpoint, int dir, int status, int length) { +void usb_core_transfer_complete(int ep, int dir, int status, int length) { #ifdef USB_BATCH_NON_NATIVE /* batch api */ - if(batch_ep != 0 && (endpoint | dir) == batch_ep) { + if(batch_ep != 0 && (ep | dir) == batch_ep) { batch_xfer_complete(); return; } #endif /* Fast notification */ - fast_completion_handler_t handler = ep_data[endpoint].fast_completion_handler[EP_DIR(dir)]; - if(handler != NULL && handler(endpoint, dir, status, length)) { + struct usb_class_driver* driver = ep_data[ep][EP_DIR(dir)].driver; + if(driver && driver->fast_transfer_complete && driver->fast_transfer_complete(ep, dir, status, length)) { return; /* do not dispatch to the queue if handled */ } /* Non-control packet handling */ - if(endpoint != EP_CONTROL) { - signal_xfer_complete(endpoint | dir, NULL, status, length); + if(ep != EP_CONTROL) { + signal_xfer_complete(ep | dir, NULL, status, length); return; } From 078a506dfd0deb18165a3ed80c7fcbdb3afb0d31 Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Mon, 6 Jul 2026 22:47:01 +0100 Subject: [PATCH 009/408] hibyr1: route ADB through the usb-mode setting hibyr1 defines HAVE_USB_ADB so the USB mode setting offers an ADB entry, but hiby_set_usb_mode() never handled USB_MODE_ADB (it fell through to default), so selecting ADB did nothing. Wire USB_MODE_ADB up to enable_adb(). It builds the adb function on the gadget and hands the functionfs mount and adbd to the vendor respawner /sbin/adbserver.sh, rather than mounting functionfs inline (which the original code flagged as flaky); adbd binds the UDC itself, so usb_enable() leaves the UDC alone in ADB mode. disable_adb() unlinks the adb function and unmounts its functionfs but leaves the function in place to be reused, matching how adbserver.sh cycles it, so re-enabling adb never re-creates it. The mass-storage LUN is removable, so the host clears its backing file when it ejects the volume (Linux does this on unmount). Re-arm the LUN in usb_enable() on each connect, so the disk is exported on every insert and not only the first. Assisted-by: Claude Opus 4.8 Change-Id: Ia8f119a1a599e6dd3c1219cb4e193c753725a06a --- firmware/target/hosted/hiby/usb-hiby-gadget.c | 81 +++++++++++-------- firmware/target/hosted/sysfs.c | 2 +- firmware/target/hosted/sysfs.h | 2 +- 3 files changed, 48 insertions(+), 37 deletions(-) diff --git a/firmware/target/hosted/hiby/usb-hiby-gadget.c b/firmware/target/hosted/hiby/usb-hiby-gadget.c index 651b25bb70..af04690248 100644 --- a/firmware/target/hosted/hiby/usb-hiby-gadget.c +++ b/firmware/target/hosted/hiby/usb-hiby-gadget.c @@ -65,6 +65,9 @@ void hiby_set_usb_mode(int mode) { usb_init_device(); } + if (_usb_mode == mode) + return; + switch(mode) { case USB_MODE_MASS_STORAGE: logf("Enabling Mass Storage\n"); @@ -74,6 +77,10 @@ void hiby_set_usb_mode(int mode) { logf("Enabling Charge\n"); enable_charging(); break; + case USB_MODE_ADB: + logf("Enabling ADB\n"); + enable_adb(); + break; default: break; } @@ -90,11 +97,29 @@ int usb_detect(void) return power_input_status() == POWER_INPUT_USB_CHARGER ? USB_INSERTED : USB_EXTRACTED; } +static void set_mass_storage_lun(void) +{ + const char *device = "/dev/mmcblk0p1"; + + // If partition 1 doesn't exist we'll try the main device + if (access(device, F_OK) != 0) + device = "/dev/mmcblk0"; + + sysfs_set_string("/sys/kernel/config/usb_gadget/adb_demo/functions/mass_storage.0/lun.0/file", device); +} + void usb_enable(bool on) { logf(">>>>>>>>>>>>>>>>> usb_enable(%d)\n", on); logf("usb enable %d %d\n", on, _usb_mode); + if (_usb_mode == USB_MODE_ADB) + return; + + // Re-arm the LUN on each connect, otherwise the disk is only exported the first time. + if (on && _usb_mode == USB_MODE_MASS_STORAGE) + set_mass_storage_lun(); + sysfs_set_string("/sys/kernel/config/usb_gadget/adb_demo/UDC", on ? "13500000.otg_new" : "\n"); } @@ -141,51 +166,49 @@ void enable_charging(void) { disable_adb(); } -#if 1 void enable_adb(void) { logf(">>>>>>>>>>>>>>>>> set_adb()\n"); // Disable mass storage if it was running disable_mass_storage(); - // Remove any lingering adb daemon - system("killall -9 adbd"); + // Remove any lingering adb daemon and its respawner + system("killall adbserver.sh adbd 2>/dev/null"); system("mkdir -p /sys/kernel/config/usb_gadget/adb_demo/configs/c.1/strings/0x409"); system("mkdir -p /sys/kernel/config/usb_gadget/adb_demo/functions/ffs.adb"); + // Use the adb VID/PID so the host recognises the device + sysfs_set_string("/sys/kernel/config/usb_gadget/adb_demo/idVendor", "0x18d1"); + sysfs_set_string("/sys/kernel/config/usb_gadget/adb_demo/idProduct", "0xd002"); + // Now we'll override configuration and MaxPower sysfs_set_string("/sys/kernel/config/usb_gadget/adb_demo/configs/c.1/strings/0x409/configuration", "adb"); sysfs_set_int("/sys/kernel/config/usb_gadget/adb_demo/configs/c.1/MaxPower", 120); // And link up the adb function to the usb gadget config - system("ln -s /sys/kernel/config/usb_gadget/adb_demo/functions/ffs.adb /sys/kernel/config/usb_gadget/adb_demo/configs/c.1/"); + system("ln -sf /sys/kernel/config/usb_gadget/adb_demo/functions/ffs.adb /sys/kernel/config/usb_gadget/adb_demo/configs/c.1/"); - int is_mounted = !system("mountpoint -q /dev/usb-ffs/adb"); + system("mkdir -p /dev/usb-ffs/adb"); - if (!is_mounted) { - system("mkdir -p /dev/usb-ffs/adb"); - /* This seems to fail, but adb will still work and then it will be mounted. Not sure what's up here... */ - system("mount -t functionfs adb /dev/usb-ffs/adb"); - } - - // Boot adb daemon - system("/usr/bin/adbd &"); + // Let the vendor respawner mount functionfs and run adbd, which binds the UDC. + system("/sbin/adbserver.sh 440 &"); } void disable_adb(void) { - // Remove any lingering adb daemon - system("killall -9 adbd"); + // Remove any lingering adb daemon and its respawner + system("killall adbserver.sh adbd 2>/dev/null"); - // Remove the adb link to config - if (access("/sys/kernel/config/usb_gadget/adb_demo/configs/c.1/ffs.adb", F_OK) == 0) { - system("rm /sys/kernel/config/usb_gadget/adb_demo/configs/c.1/ffs.adb"); + // Unbind the UDC so the gadget can be reconfigured + if (access("/sys/kernel/config/usb_gadget/adb_demo/UDC", F_OK) == 0) { + sysfs_set_string("/sys/kernel/config/usb_gadget/adb_demo/UDC", "\n"); } - // Remove the adb function - if (access("/sys/kernel/config/usb_gadget/adb_demo/functions/ffs.adb", F_OK) == 0) { - system("rm -rf /sys/kernel/config/usb_gadget/adb_demo/functions/ffs.adb"); + // Unlink the adb function from config (the function is left in place and + // reused, so re-creating it in enable_adb can't block) + if (access("/sys/kernel/config/usb_gadget/adb_demo/configs/c.1/ffs.adb", F_OK) == 0) { + system("rm /sys/kernel/config/usb_gadget/adb_demo/configs/c.1/ffs.adb"); } // Reset the MaxPower to its default value @@ -200,13 +223,9 @@ void disable_adb(void) { // Unmount adb if (!system("mountpoint -q /dev/usb-ffs/adb")) { - system("unmount /dev/usb-ffs/adb"); + system("umount -l /dev/usb-ffs/adb"); } } -#else -void enable_adb(void) {} -void disable_adb(void) {} -#endif void enable_mass_storage(void) { logf(">>>>>>>>>>>>>>>>> set_mass_storage()\n"); @@ -226,15 +245,7 @@ void enable_mass_storage(void) { system("ln -s /sys/kernel/config/usb_gadget/adb_demo/functions/mass_storage.0 /sys/kernel/config/usb_gadget/adb_demo/configs/c.1/"); - char mount_device[32] = "/dev/mmcblk0p1"; - - // If partition 1 doesn't exist we'll try the main device - if (access(mount_device, F_OK) != 0) { - memset(mount_device, 0, sizeof(mount_device)); - strcpy(mount_device, "/dev/mmcblk0"); - } - - sysfs_set_string("/sys/kernel/config/usb_gadget/adb_demo/functions/mass_storage.0/lun.0/file", mount_device); + set_mass_storage_lun(); } void disable_mass_storage(void) { diff --git a/firmware/target/hosted/sysfs.c b/firmware/target/hosted/sysfs.c index 1c3fe8c396..7bacb3de54 100644 --- a/firmware/target/hosted/sysfs.c +++ b/firmware/target/hosted/sysfs.c @@ -178,7 +178,7 @@ bool sysfs_get_string(const char *path, char *value, int size) } -bool sysfs_set_string(const char *path, char *value) +bool sysfs_set_string(const char *path, const char *value) { FILE *f = open_write(path); if(f == NULL) diff --git a/firmware/target/hosted/sysfs.h b/firmware/target/hosted/sysfs.h index 639cc1c409..de2b8db31e 100644 --- a/firmware/target/hosted/sysfs.h +++ b/firmware/target/hosted/sysfs.h @@ -28,4 +28,4 @@ bool sysfs_set_int(const char *path, int value); bool sysfs_get_char(const char *path, char *value); bool sysfs_set_char(const char *path, char value); bool sysfs_get_string(const char *path, char *value, int size); -bool sysfs_set_string(const char *path, char *value); +bool sysfs_set_string(const char *path, const char *value); From 0c29c97f1ae48a3869c7a3bd3abe3ea5d00d6d71 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Wed, 15 Jul 2026 15:41:24 +0300 Subject: [PATCH 010/408] hiby: r1: fix headphone detection Change-Id: I4fdf47fb3585754428f6be804e65993e965c3914 --- firmware/target/hosted/hiby/hibylinux_codec.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/firmware/target/hosted/hiby/hibylinux_codec.c b/firmware/target/hosted/hiby/hibylinux_codec.c index 0464dceaf2..ba338f5410 100644 --- a/firmware/target/hosted/hiby/hibylinux_codec.c +++ b/firmware/target/hosted/hiby/hibylinux_codec.c @@ -65,11 +65,11 @@ int hiby_has_valid_output(void) { const char * const sysfs_hs_switch = "/sys/class/switch/headset/state"; const char * const sysfs_bal_switch = "/sys/class/switch/balance/state"; - sysfs_get_int(sysfs_hs_switch, &status); - if (status) ps = 2; // headset + if (sysfs_get_int(sysfs_hs_switch, &status) && status) + ps = 2; // headset - sysfs_get_int(sysfs_bal_switch, &status); - if (status) ps = 3; // balanced output + if (sysfs_get_int(sysfs_bal_switch, &status) && status) + ps = 3; // balanced output return ps; } From 6c4107d1ad0523c0845ae68984f1b553edf94f90 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Wed, 15 Jul 2026 16:22:18 -0400 Subject: [PATCH 011/408] libspeex: Silence a spurious warning under Clang Change-Id: I9790e92dd9120afff83114925b8e6eb482d6e7df --- lib/rbcodec/codecs/libspeex/ltp.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/rbcodec/codecs/libspeex/ltp.c b/lib/rbcodec/codecs/libspeex/ltp.c index 4e4d651d8f..77fb17b050 100644 --- a/lib/rbcodec/codecs/libspeex/ltp.c +++ b/lib/rbcodec/codecs/libspeex/ltp.c @@ -814,7 +814,9 @@ spx_word32_t *cumul_gain for (i=0;i Date: Wed, 15 Jul 2026 21:15:51 -0400 Subject: [PATCH 012/408] rbutil: Ignore the deprecated warnings for macos builds A replacement is in progress, so let's ignore those warnings in the mean time Change-Id: Ic3123d2b37a21c6c0570619c387de0f1141c70f1 --- utils/rbutilqt/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/utils/rbutilqt/CMakeLists.txt b/utils/rbutilqt/CMakeLists.txt index b3b564510f..f35eefb1ad 100644 --- a/utils/rbutilqt/CMakeLists.txt +++ b/utils/rbutilqt/CMakeLists.txt @@ -246,6 +246,12 @@ if(APPLE) target_sources(rbbase PRIVATE ${CMAKE_CURRENT_LIST_DIR}/base/ttscarbon.cpp ${CMAKE_CURRENT_LIST_DIR}/base/ttscarbon.h) + + # The below is only needed for ttscarbon + # So we can delete this whole if(APPLE) block at once + target_compile_options(rbbase PRIVATE + $<$:-Wno-deprecated-declarations> + ) endif() if (TARGET Qt${QT_VERSION_MAJOR}::TextToSpeech AND TARGET Qt${QT_VERSION_MAJOR}::Multimedia) From f20086c91aa2c9e2f20b4f7e34c96b34347fcaa1 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Sat, 6 Jun 2026 16:11:27 +0100 Subject: [PATCH 013/408] asm: ARMv7-M: replace unity-gain write_samples() asm with memcpy Change-Id: I164894d944136474b1b6ba1ce106f163a5db9642 --- firmware/asm/arm/pcm-mixer-armv6.c | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/firmware/asm/arm/pcm-mixer-armv6.c b/firmware/asm/arm/pcm-mixer-armv6.c index 613a9ed25a..450a285689 100644 --- a/firmware/asm/arm/pcm-mixer-armv6.c +++ b/firmware/asm/arm/pcm-mixer-armv6.c @@ -76,25 +76,7 @@ static FORCE_INLINE void write_samples(void *out, { /* Channel is unity amplitude */ #if defined(CPU_ARM_MICRO) && ARCH_VERSION >= 7 - asm volatile ( - "ands r4, %2, #0x1f \n" - "beq 2f \n" - "1: \n" - "ldr r3, [%1], #4 \n" - "subs r4, r4, #4 \n" - "str r3, [%0], #4 \n" - "bne 1b \n" - "bics %2, %2, #0x1f \n" - "beq 3f \n" - "2: \n" - "ldmia %1!, { r3-r10 } \n" - "subs %2, %2, #32 \n" - "stmia %0!, { r3-r10 } \n" - "bhi 2b \n" - "3: \n" - : "+lr"(out), "+lr"(src), "+r"(size) - : - : "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10"); + memcpy(out, src, size); #else asm volatile ( "ands r1, %2, #0x1f \n" From 91d49adce2cc2661265ae7d3affeefdf7fb84f86 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sat, 11 Jul 2026 11:37:37 -0400 Subject: [PATCH 014/408] rbutil: Silence the final build warnings when building with Clang Change-Id: I65bb37993ef0ef6a0f373711ea16a417d0839cc5 --- utils/rbutilqt/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/utils/rbutilqt/CMakeLists.txt b/utils/rbutilqt/CMakeLists.txt index f35eefb1ad..2a4fcfcfd9 100644 --- a/utils/rbutilqt/CMakeLists.txt +++ b/utils/rbutilqt/CMakeLists.txt @@ -254,6 +254,12 @@ if(APPLE) ) endif() +if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(RockboxUtility PRIVATE + $<$:-Wno-c++20-extensions> + ) +endif() + if (TARGET Qt${QT_VERSION_MAJOR}::TextToSpeech AND TARGET Qt${QT_VERSION_MAJOR}::Multimedia) target_sources(rbbase PRIVATE ${CMAKE_CURRENT_LIST_DIR}/base/ttsqt.cpp From c35271a3f060dc16e63786d5cf34510774910e39 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Thu, 16 Jul 2026 08:30:26 -0400 Subject: [PATCH 015/408] themeeditor: Silence a build warning The warning comes from code generated by QT's MOC, so there isn't anything to be done until QT addresses this properly. Change-Id: Ia88ac9af91acbab783b683a6f0b4f4a304aa9e4d --- utils/themeeditor/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/utils/themeeditor/CMakeLists.txt b/utils/themeeditor/CMakeLists.txt index 8720ada7c3..5608ecf13e 100644 --- a/utils/themeeditor/CMakeLists.txt +++ b/utils/themeeditor/CMakeLists.txt @@ -120,6 +120,11 @@ target_link_libraries(RockboxThemeEditor target_include_directories(RockboxThemeEditor PRIVATE models graphics gui qtfindreplacedialog quazip) target_compile_definitions(RockboxThemeEditor PRIVATE FINDREPLACE_NOLIB) +# This is an issue with the code produced by QT's MOC +if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") +target_compile_options(RockboxThemeEditor PRIVATE -Wsfinae-incomplete=0) +endif() + if(APPLE) # MacOS Application Bundle specifics. target_sources(RockboxThemeEditor PRIVATE From 8923f479d0fabe94ac585b646b8d4d376a174918 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Wed, 15 Jul 2026 15:50:10 +0300 Subject: [PATCH 016/408] hiby: r3proii: fix crash on playback when no headphones are detected 0 is an invalid value to set on Hiby as "Output Port Switch". For devices with multiple outputs, we need to ensure it is set to a valid port. Change-Id: I16ea2e620fae3034dccf3267d316924d8e2c0a95 --- firmware/target/hosted/hiby/hibylinux_codec.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/firmware/target/hosted/hiby/hibylinux_codec.c b/firmware/target/hosted/hiby/hibylinux_codec.c index ba338f5410..4cb445887d 100644 --- a/firmware/target/hosted/hiby/hibylinux_codec.c +++ b/firmware/target/hosted/hiby/hibylinux_codec.c @@ -86,6 +86,12 @@ void hiby_set_output(int ps) { if (!hw_init || muted) return; + // Default to headset if nothing was ever inserted; otherwise, R3 Pro II crashes on playback + if (ps == 0) + { + ps = last_ps > 0 ? last_ps : 2; + } + if (last_ps != ps) { logf("set out %d/%d", ps, last_ps); From b481c4536e4cc44c38f3a05bbb14ef0cf07d7e9b Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Fri, 10 Jul 2026 23:16:07 -0400 Subject: [PATCH 017/408] FS#13954 - Extra semicolon in file paths in get_files module date and size still have them to separate the filename from attribs Change-Id: I3fcaa7061d14493dab178d68398bea25f8ca67d6 --- apps/plugins/lua/include_lua/get_files.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/plugins/lua/include_lua/get_files.lua b/apps/plugins/lua/include_lua/get_files.lua index f9e7a66699..42c6c9c378 100644 --- a/apps/plugins/lua/include_lua/get_files.lua +++ b/apps/plugins/lua/include_lua/get_files.lua @@ -29,7 +29,7 @@ if ... == nil then rb.splash(rb.HZ * 3, "use 'require'") end or you can provide your own function see below.. -- sort_by can be by "name" "size" "date" or "none" to perform no sorting note: for "size" and "date" you may need to strip the attribute data to use - the returned filename + the returned filename e.g. string.match(file, "[^;]+") -- cancel_fn if not defined or not a function no user cancel otherwise supply your own cancel function which returns true to cancel searching for files -- f_t and d_t allow you to pass your own tables for re-use but isn't necessary @@ -109,7 +109,7 @@ local function get_files(path, recurse, finddir, findfile, sort_by, cancel_fn, f if sort_by == "name" then sort_by_function = function(s1, s2) return s1 < s2 end filepath_function = function(path, sep, fname, fattrib, fsize, ftime) - return string.format("%s%s%s;", path, sep, fname) + return string.format("%s%s%s", path, sep, fname) end elseif sort_by == "size" then filepath_function = function(path, sep, fname, fattrib, fsize, ftime) @@ -139,7 +139,7 @@ local function get_files(path, recurse, finddir, findfile, sort_by, cancel_fn, f end else -- "none" filepath_function = function(path, sep, fname, fattrib, fsize, ftime) - return string.format("%s%s%s;", path, sep, fname) + return string.format("%s%s%s", path, sep, fname) end end @@ -147,7 +147,7 @@ local function get_files(path, recurse, finddir, findfile, sort_by, cancel_fn, f for key,value in pairs(dirs) do --luadir.dir may error out so we need to do the call protected - -- _get_files(value, CANCEL_BUTTON) + -- _get_files(value) _, quit = pcall(_get_files, value) if quit == true or not recurse then From 3ad3fba68e835890787d346998726a50cd6990b7 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Sun, 12 Jul 2026 12:59:18 -0400 Subject: [PATCH 018/408] filebrowse.lua don't display parent path for files when browsing the filename gets cut off due to the length of the parent path Instead strip the parent from the beginning for files Change-Id: Ib9dadffabced89b5895420cb696227ca8cc444a2 --- apps/plugins/lua/include_lua/filebrowse.lua | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/plugins/lua/include_lua/filebrowse.lua b/apps/plugins/lua/include_lua/filebrowse.lua index fb74798ccb..c41598fd63 100755 --- a/apps/plugins/lua/include_lua/filebrowse.lua +++ b/apps/plugins/lua/include_lua/filebrowse.lua @@ -89,13 +89,13 @@ function file_choose(dir, title, sort_by, descending) if not descending then for i = 1, #files do - -- only store file name .. strip attributes from end - table.insert(dirs, "\t" .. string.match(files[i], "[^;]+") or "?") + -- only store file name .. strip attributes from end and parent from beginning + table.insert(dirs, "\t" .. string.match(string.match(files[i], "[^/\\]+$") or "?", "[^;]+") or "?") end else for i = #files, 1, -1 do - -- only store file name .. strip attributes from end - table.insert(dirs, "\t" .. string.match(files[i], "[^;]+") or "?") + -- only store file name .. strip attributes from end and parent from beginning + table.insert(dirs, "\t" .. string.match(string.match(files[i], "[^/\\]+$") or "?", "[^;]+") or "?") end end for i=1, #files do files[i] = nil end -- empty table for reuse @@ -112,7 +112,11 @@ function file_choose(dir, title, sort_by, descending) if item > 0 then dir = string.gsub(dirs[item], "%c+","") if not rb.dir_exists("/" .. dir) then - return dir + if (parentdir == "/") then + return parentdir .. dir + else + return parentdir .. "/" .. dir + end end end From f58038b687a6bca266334f279a8611f8e1605c67 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Fri, 17 Jul 2026 10:26:56 -0400 Subject: [PATCH 019/408] luadir small cleanuo luadir stores if a file was closed, we can get this by checking for a NULL file handle instead Change-Id: I95b8cc84afcdfc8515af8012afb1df64d0987fc4 --- apps/plugins/lua/luadir.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/plugins/lua/luadir.c b/apps/plugins/lua/luadir.c index cf0b4a9f8b..1f8c929239 100644 --- a/apps/plugins/lua/luadir.c +++ b/apps/plugins/lua/luadir.c @@ -31,7 +31,6 @@ #define DIR_METATABLE "directory metatable" typedef struct dir_data { - int closed; DIR *dir; } dir_data; @@ -55,7 +54,7 @@ static int dir_iter (lua_State *L) { struct dirent *entry; dir_data *d = (dir_data *)luaL_checkudata (L, 1, DIR_METATABLE); - luaL_argcheck (L, !d->closed, 1, "closed directory"); + luaL_argcheck (L, d->dir != NULL, 1, "closed directory"); if ((entry = readdir (d->dir)) != NULL) { struct dirinfo info = dir_get_info(d->dir, entry); @@ -78,7 +77,7 @@ static int dir_iter (lua_State *L) { } else { /* no more entries => close directory */ closedir (d->dir); - d->closed = 1; + d->dir = NULL; return 0; } } @@ -90,9 +89,9 @@ static int dir_iter (lua_State *L) { static int dir_close (lua_State *L) { dir_data *d = (dir_data *)lua_touserdata (L, 1); - if (!d->closed && d->dir) { + if (d->dir) { closedir (d->dir); - d->closed = 1; + d->dir = NULL; } return 0; } @@ -107,7 +106,6 @@ static int dir_iter_factory (lua_State *L) { lua_settop(L, 2); /* index 2 (bool) return attribute table */ lua_pushcclosure(L, &dir_iter, 1); d = (dir_data *) lua_newuserdata (L, sizeof(dir_data)); - d->closed = 0; luaL_getmetatable (L, DIR_METATABLE); lua_setmetatable (L, -2); From 943b73851e9f5cda502ba68121f6e1cfb5d80fc3 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Wed, 15 Jul 2026 22:26:42 +0100 Subject: [PATCH 020/408] playback: prevent crossfade of new track after pause (FS#13958) Wipe the PCM buffer when starting playback from a paused state. This prevents unwanted crossfade when manually pausing and then selecting a new track. Change-Id: I55e21004c12a2cd67417b627d471e3da115b7077 --- apps/playback.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/playback.c b/apps/playback.c index 719802e030..6e4e7b2dc3 100644 --- a/apps/playback.c +++ b/apps/playback.c @@ -3033,12 +3033,18 @@ static void audio_start_playback(const struct audio_resume_info *resume_info, track_event_flags = TEF_NONE; ff_rw_mode = false; + /* + * When restarting playback or resuming from a paused state, + * the PCM buffer should be cleared to ensure crossfade will + * not be performed. + */ + if ((flags & AUDIO_START_RESTART) || old_status == PLAY_PAUSED) + pcmbuf_play_stop(); + if (flags & AUDIO_START_RESTART) { /* Clear out some stuff to resume the current track where it left off */ - pcmbuf_play_stop(); - resume.elapsed = id3_get(PLAYING_ID3)->elapsed; resume.offset = id3_get(PLAYING_ID3)->offset; skip_resume_adjustments = id3_get(PLAYING_ID3)->skip_resume_adjustments; From 0ca801dffa41a1b5112338f1de37cce3c1171482 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Sun, 19 Jul 2026 09:23:36 +0100 Subject: [PATCH 021/408] themeeditor: only add -Wsfinae-incomplete if compiler supports it This flag was added in GCC 16; older versions of GCC don't accept it and don't produce the warning, so they don't need any special handling. Change-Id: I13fcba07cafe0b4133de8b03ede681d2454249e4 --- utils/themeeditor/CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utils/themeeditor/CMakeLists.txt b/utils/themeeditor/CMakeLists.txt index 5608ecf13e..8493793d71 100644 --- a/utils/themeeditor/CMakeLists.txt +++ b/utils/themeeditor/CMakeLists.txt @@ -122,7 +122,10 @@ target_compile_definitions(RockboxThemeEditor PRIVATE FINDREPLACE_NOLIB) # This is an issue with the code produced by QT's MOC if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") -target_compile_options(RockboxThemeEditor PRIVATE -Wsfinae-incomplete=0) + check_cxx_compiler_flag(-Wsfinae-incomplete=0 HAVE_SFINAE_INCOMPLETE) + if (HAVE_SFINAE_INCOMPLETE) + target_compile_options(RockboxThemeEditor PRIVATE -Wsfinae-incomplete=0) + endif() endif() if(APPLE) From f463f4a17a9441def4e51cd1568d7fd36d261749 Mon Sep 17 00:00:00 2001 From: Skye Date: Wed, 20 May 2026 18:04:01 +0900 Subject: [PATCH 022/408] audiohw: remove unused BALANCE_CAP and CLIPPING_CAP Change-Id: I8905e9a656fcba20c8adeb840d148bcd97511665 --- firmware/drivers/audio/ctru.c | 3 -- firmware/drivers/audio/sdl.c | 3 -- firmware/export/audiohw.h | 46 +++++-------------- firmware/sound.c | 16 ------- .../target/hosted/android/android-codec.c | 5 -- 5 files changed, 12 insertions(+), 61 deletions(-) diff --git a/firmware/drivers/audio/ctru.c b/firmware/drivers/audio/ctru.c index 05cca2e76f..b973b1fb87 100644 --- a/firmware/drivers/audio/ctru.c +++ b/firmware/drivers/audio/ctru.c @@ -37,9 +37,6 @@ void audiohw_set_volume(int vol_l, int vol_r) (void)vol_l; (void)vol_r; } -#if defined(AUDIOHW_HAVE_BALANCE) -void audiohw_set_balance(int value) { (void)value; } -#endif #ifndef HAVE_SW_TONE_CONTROLS #if defined(AUDIOHW_HAVE_BASS) void audiohw_set_bass(int value) { (void)value; } diff --git a/firmware/drivers/audio/sdl.c b/firmware/drivers/audio/sdl.c index 7250f36124..d85fba9933 100644 --- a/firmware/drivers/audio/sdl.c +++ b/firmware/drivers/audio/sdl.c @@ -75,9 +75,6 @@ void audiohw_set_prescaler(int value) } #endif /* AUDIOHW_HAVE_PRESCALER */ -#if defined(AUDIOHW_HAVE_BALANCE) -void audiohw_set_balance(int value) { (void)value; } -#endif #ifndef HAVE_SW_TONE_CONTROLS #if defined(AUDIOHW_HAVE_BASS) void audiohw_set_bass(int value) { (void)value; } diff --git a/firmware/export/audiohw.h b/firmware/export/audiohw.h index 25e6f46b3d..c6308b3aff 100644 --- a/firmware/export/audiohw.h +++ b/firmware/export/audiohw.h @@ -28,19 +28,17 @@ /* define some audiohw caps */ #define TREBLE_CAP (1 << 0) #define BASS_CAP (1 << 1) -#define BALANCE_CAP (1 << 2) -#define CLIPPING_CAP (1 << 3) -#define PRESCALER_CAP (1 << 4) -#define BASS_CUTOFF_CAP (1 << 5) -#define TREBLE_CUTOFF_CAP (1 << 6) -#define EQ_CAP (1 << 7) -#define DEPTH_3D_CAP (1 << 8) -#define LINEOUT_CAP (1 << 9) -#define MONO_VOL_CAP (1 << 10) -#define LIN_GAIN_CAP (1 << 11) -#define MIC_GAIN_CAP (1 << 12) -#define FILTER_ROLL_OFF_CAP (1 << 13) -#define POWER_MODE_CAP (1 << 14) +#define PRESCALER_CAP (1 << 2) +#define BASS_CUTOFF_CAP (1 << 3) +#define TREBLE_CUTOFF_CAP (1 << 4) +#define EQ_CAP (1 << 5) +#define DEPTH_3D_CAP (1 << 6) +#define LINEOUT_CAP (1 << 7) +#define MONO_VOL_CAP (1 << 8) +#define LIN_GAIN_CAP (1 << 9) +#define MIC_GAIN_CAP (1 << 10) +#define FILTER_ROLL_OFF_CAP (1 << 11) +#define POWER_MODE_CAP (1 << 12) /* Used by every driver to export its min/max/default values for its audio settings. */ @@ -270,14 +268,6 @@ struct sound_settings_info #define AUDIOHW_HAVE_TREBLE_CUTOFF #endif -#if (AUDIOHW_CAPS & BALANCE_CAP) -#define AUDIOHW_HAVE_BALANCE -#endif - -#if (AUDIOHW_CAPS & CLIPPING_CAP) -#define AUDIOHW_HAVE_CLIPPING -#endif - #if (AUDIOHW_CAPS & PRESCALER_CAP) #define AUDIOHW_HAVE_PRESCALER #endif @@ -460,7 +450,7 @@ void audiohw_close(void); * Set new volume value * @param val to set in centibels. * NOTE: AUDIOHW_CAPS need to contain - * CLIPPING_CAP + * MONO_VOL_CAP */ void audiohw_set_volume(int val); #else /* Stereo volume */ @@ -481,7 +471,6 @@ void audiohw_set_volume(int vol_l, int vol_r); void audiohw_set_lineout_volume(int vol_l, int vol_r); #endif -#ifndef AUDIOHW_HAVE_CLIPPING #if defined(AUDIOHW_HAVE_BASS) || defined(AUDIOHW_HAVE_TREBLE) \ || defined(AUDIOHW_HAVE_EQ) /** @@ -492,17 +481,6 @@ void audiohw_set_lineout_volume(int vol_l, int vol_r); */ void audiohw_set_prescaler(int val); #endif -#endif /* !AUDIOHW_HAVE_CLIPPING */ - -#ifdef AUDIOHW_HAVE_BALANCE -/** - * Set new balance value - * @param val to set. - * NOTE: AUDIOHW_CAPS need to contain - * BALANCE_CAP - */ -void audiohw_set_balance(int val); -#endif #ifdef AUDIOHW_HAVE_TREBLE /** diff --git a/firmware/sound.c b/firmware/sound.c index 40975abfc6..c7d510dc2f 100644 --- a/firmware/sound.c +++ b/firmware/sound.c @@ -196,7 +196,6 @@ int sound_current(int setting) } /* switch(setting) */ }/* sound_current */ -#if !defined(AUDIOHW_HAVE_CLIPPING) /* * The prescaler compensates for any kind of boosts, to prevent clipping. * @@ -304,7 +303,6 @@ static void set_prescaled_volume(void) audiohw_set_lineout_volume(0, 0); #endif /* AUDIOHW_HAVE_LINEOUT */ } -#endif /* AUDIOIHW_HAVE_CLIPPING */ void sound_set_volume(int value) { @@ -325,12 +323,8 @@ void sound_set_volume(int value) global_status.volume = value; #endif -#if defined(AUDIOHW_HAVE_CLIPPING) - audiohw_set_volume(value); -#else sound_prescaler.volume = sound_value_to_cb(SOUND_VOLUME, value); set_prescaled_volume(); -#endif } void sound_set_balance(int value) @@ -338,12 +332,8 @@ void sound_set_balance(int value) if (!audio_is_initialized) return; -#if defined(AUDIOHW_HAVE_BALANCE) - audiohw_set_balance(value); -#else sound_prescaler.balance = value; set_prescaled_volume(); -#endif } #if defined(AUDIOHW_HAVE_BASS) @@ -354,10 +344,8 @@ void sound_set_bass(int value) audiohw_set_bass(value); -#if !defined(AUDIOHW_HAVE_CLIPPING) sound_prescaler.bass = sound_value_to_cb(SOUND_BASS, value); set_prescaled_volume(); -#endif } #endif /* AUDIOHW_HAVE_BASS */ @@ -369,10 +357,8 @@ void sound_set_treble(int value) audiohw_set_treble(value); -#if !defined(AUDIOHW_HAVE_CLIPPING) sound_prescaler.treble = sound_value_to_cb(SOUND_TREBLE, value); set_prescaled_volume(); -#endif } #endif /* AUDIOHW_HAVE_TREBLE */ @@ -517,11 +503,9 @@ static void sound_set_hw_eq_band_gain(unsigned int band, int value) audiohw_set_eq_band_gain(band, value); -#if !defined (AUDIOHW_HAVE_CLIPPING) int setting = sound_enum_hw_eq_band_setting(band, AUDIOHW_EQ_GAIN); sound_prescaler.eq_gain[band] = sound_value_to_cb(setting, value); set_prescaled_volume(); -#endif /* AUDIOHW_HAVE_CLIPPING */ } void sound_set_hw_eq_band1_gain(int value) diff --git a/firmware/target/hosted/android/android-codec.c b/firmware/target/hosted/android/android-codec.c index 6e069210df..794ad947ed 100644 --- a/firmware/target/hosted/android/android-codec.c +++ b/firmware/target/hosted/android/android-codec.c @@ -28,11 +28,6 @@ void audiohw_set_volume(int volume) pcm_set_mixer_volume(volume); } -void audiohw_set_balance(int balance) -{ - (void)balance; -} - void audiohw_close(void) { pcm_shutdown(); From 91774611cd17bb7e6363dc62b989c134782fcec1 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 21 Jul 2026 14:55:06 -0400 Subject: [PATCH 023/408] uisimulator: Get rid of long-obsolete stub for mpeg_set_pitch() Change-Id: Icbfa6917d0108d12a4428c8fe512c0a48385cdaa --- uisimulator/common/stubs.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/uisimulator/common/stubs.c b/uisimulator/common/stubs.c index cadead5c70..9f9dc0d4e0 100644 --- a/uisimulator/common/stubs.c +++ b/uisimulator/common/stubs.c @@ -214,11 +214,6 @@ void lcd_set_contrast( int x ) (void)x; } -void mpeg_set_pitch(int pitch) -{ - (void)pitch; -} - void cpu_sleep(bool enabled) { (void)enabled; From 8acd1c14d620f4e4b716f195346431a6e3b753ab Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Tue, 21 Jul 2026 22:08:24 +0100 Subject: [PATCH 024/408] kernel: avoid divide-by-zero in thread_get_debug_info() for zero-size stacks thread_get_debug_info() computes the current stack usage as stack_used_current * 100 / thread->stack_size with no guard against a zero stack size. On hosted targets the main thread has stack_size == 0, so opening the "View OS stacks" debug screen divides by zero and panics with a floating point exception. Guard the division and report 0% when the stack size is unknown, which matches the stack_usage() helper. Likely affects all hosted non-SDL targets, but tested/confirmed on my HiBy R1. Change-Id: I8ecc56f53f1e97e8a59ca71c0e08a66093fecbae --- firmware/kernel/thread-common.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/firmware/kernel/thread-common.c b/firmware/kernel/thread-common.c index 0170b66237..7fd071b026 100644 --- a/firmware/kernel/thread-common.c +++ b/firmware/kernel/thread-common.c @@ -309,7 +309,10 @@ int thread_get_debug_info(unsigned int thread_id, size_t stack_used_current = thread->stack_size - (thread->context.sp - (uintptr_t)thread->stack); - infop->stack_usage_cur = stack_used_current * 100 / thread->stack_size; + /* Hosted implementations typically have the main stack managed + by the OS, so we create a fake entry with 0 length */ + infop->stack_usage_cur = thread->stack_size ? + stack_used_current * 100 / thread->stack_size : 0; #endif #if NUM_CORES > 1 infop->core = thread->core; From ea775fa501e283e0f7777e71970c09e04af11093 Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Tue, 21 Jul 2026 00:38:53 +0100 Subject: [PATCH 025/408] hibyr1: add USB DAC scaffolding Introduce the generic, target-agnostic pieces for a gadget-driven USB Audio Class DAC, gated by the HAVE_HOST_USB_AUDIO target flag so they stay inert unless a target opts in. Rather than adding a new USB mode, reuse the existing usb_audio setting (never / always / while charge-only / while mass-storage) with its LANG_USB_DAC string, usb_set_audio() callback and the PCM_MIXER_CHAN_USBAUDIO mixer channel. HAVE_HOST_USB_AUDIO becomes the umbrella capability flag for this common code; USB_ENABLE_AUDIO stays the native usbstack class-driver contract (which needs HAVE_USBSTACK and so cannot be used by hosted ports whose kernel owns the USB controller). usb_set_audio() is declared here and implemented by the target (natively by the usbstack driver, or in the hosted port for gadget targets, as with usb_audio_get_active()), and applied at settings load. The playback interlock in wps.c/playback.c keeps local playback from mixing with host audio while the DAC is active. Simulator stubs for usb_set_audio() and usb_audio_get_active() keep sim builds linking. Co-Authored-By: Claude Opus 4.8 Change-Id: I5b7738508c24721d91d4c32379102a1ac7c6228b --- apps/features.txt | 2 +- apps/gui/wps.c | 2 +- apps/menus/settings_menu.c | 6 +++--- apps/playback.c | 5 ++++- apps/settings.c | 3 +++ apps/settings.h | 2 +- apps/settings_list.c | 2 +- firmware/export/pcm_mixer.h | 2 +- firmware/export/usb.h | 9 ++++++++- uisimulator/common/stubs.c | 12 ++++++++++++ 10 files changed, 35 insertions(+), 10 deletions(-) diff --git a/apps/features.txt b/apps/features.txt index 2d3ad26a73..5453ec86bc 100644 --- a/apps/features.txt +++ b/apps/features.txt @@ -313,7 +313,7 @@ codec_aac_he lto #endif -#if defined(USB_ENABLE_AUDIO) +#if defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) usbdac #endif diff --git a/apps/gui/wps.c b/apps/gui/wps.c index add47d08db..dec58008b8 100644 --- a/apps/gui/wps.c +++ b/apps/gui/wps.c @@ -696,7 +696,7 @@ static inline int action_wpsab_single(long button) long gui_wps_show(void) { /* NOTE: if USBAudio ever gets its own DSP channel, this block can go away! */ -#ifdef USB_ENABLE_AUDIO +#if defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) if (usb_audio_get_active()) { splash(HZ*2, ID2P(LANG_USB_DAC_ACTIVE)); diff --git a/apps/menus/settings_menu.c b/apps/menus/settings_menu.c index 9b017dc2f9..1879ada8fe 100644 --- a/apps/menus/settings_menu.c +++ b/apps/menus/settings_menu.c @@ -368,7 +368,7 @@ MENUITEM_SETTING(lineout_onoff, &global_settings.lineout_active, NULL); MENUITEM_SETTING(usb_hid, &global_settings.usb_hid, NULL); MENUITEM_SETTING(usb_keypad_mode, &global_settings.usb_keypad_mode, NULL); #endif -#ifdef USB_ENABLE_AUDIO +#if defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) MENUITEM_SETTING(usb_audio, &global_settings.usb_audio, NULL); #endif #if defined(USB_ENABLE_STORAGE) && defined(HAVE_MULTIDRIVE) @@ -421,7 +421,7 @@ MAKE_MENU(usb_menu, ID2P(LANG_USB_MENU), 0, Icon_NOICON, &usb_hid, &usb_keypad_mode, #endif -#ifdef USB_ENABLE_AUDIO +#if defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) &usb_audio, #endif #if defined(USB_ENABLE_STORAGE) && defined(HAVE_MULTIDRIVE) @@ -480,7 +480,7 @@ MAKE_MENU(system_menu, ID2P(LANG_SYSTEM), &sel_softlock, #endif #if defined(USB_ENABLE_HID) || \ - defined(USB_ENABLE_AUDIO) || \ + defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) || \ (defined(USB_ENABLE_STORAGE) && defined(HAVE_MULTIDRIVE)) || \ defined(HAVE_USB_MODE) &usb_menu, diff --git a/apps/playback.c b/apps/playback.c index 6e4e7b2dc3..ccb9aa1c8c 100644 --- a/apps/playback.c +++ b/apps/playback.c @@ -66,8 +66,11 @@ #include /* For strncasecmp() */ #endif +#if defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) #ifdef USB_ENABLE_AUDIO #include "usbstack/usb_audio.h" +#endif +#include "usb.h" #include "splash.h" #include "lang.h" #endif @@ -2993,7 +2996,7 @@ static void audio_start_playback(const struct audio_resume_info *resume_info, * a splash message to the user. * NOTE: if USBAudio ever gets its own DSP channel, this block can go away! */ -#ifdef USB_ENABLE_AUDIO +#if defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) if (usb_audio_get_active()) { queue_reply(&audio_queue, 0); diff --git a/apps/settings.c b/apps/settings.c index afe62299e3..0423eebd29 100644 --- a/apps/settings.c +++ b/apps/settings.c @@ -1128,6 +1128,9 @@ void settings_apply(bool read_disk) #if !defined(SIMULATOR) && !defined(USB_NONE) && (defined(HAVE_USB_ADB) || defined(HAVE_USB_POWER)) usb_set_mode(global_settings.usb_mode); #endif +#if defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) + usb_set_audio(global_settings.usb_audio); +#endif #if defined(HAVE_GENERAL_PURPOSE_LED) led_hw_on(); #endif diff --git a/apps/settings.h b/apps/settings.h index 178817f280..a2ce824a5e 100644 --- a/apps/settings.h +++ b/apps/settings.h @@ -814,7 +814,7 @@ struct user_settings int usb_keypad_mode; #endif -#ifdef USB_ENABLE_AUDIO +#if defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) int usb_audio; #endif diff --git a/apps/settings_list.c b/apps/settings_list.c index 1f24a8e0af..0887e0527a 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -2310,7 +2310,7 @@ const struct settings_list settings[] = { ), /* CHOICE_SETTING( usb_keypad_mode ) */ #endif -#ifdef USB_ENABLE_AUDIO +#if defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) CHOICE_SETTING(0, usb_audio, LANG_USB_DAC, 0, "usb-dac", "never,always,while_charge_only,while_mass_storage", usb_set_audio, 4, ID2P(LANG_NEVER), ID2P(LANG_ALWAYS), ID2P(LANG_WHILE_USB_CHARGE_ONLY), ID2P(LANG_WHILE_MASS_STORAGE_USB_ONLY)), #endif diff --git a/firmware/export/pcm_mixer.h b/firmware/export/pcm_mixer.h index a003100285..bbc2c2c9cd 100644 --- a/firmware/export/pcm_mixer.h +++ b/firmware/export/pcm_mixer.h @@ -71,7 +71,7 @@ enum pcm_mixer_channel { PCM_MIXER_CHAN_PLAYBACK = 0, -#ifdef USB_ENABLE_AUDIO +#if defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) PCM_MIXER_CHAN_USBAUDIO, #endif PCM_MIXER_CHAN_VOICE, diff --git a/firmware/export/usb.h b/firmware/export/usb.h index a324b1174c..aa1e25c93b 100644 --- a/firmware/export/usb.h +++ b/firmware/export/usb.h @@ -272,8 +272,15 @@ void usb_firewire_connect_event(void); void usb_set_hid(bool enable); #endif -#ifdef USB_ENABLE_AUDIO +#if defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) +/* Select when the USB Audio (DAC) function is active. Values follow the + * usb_audio setting: 0 never, 1 always, 2 while charge-only, 3 while + * mass-storage. Native targets use the usbstack driver; hosted targets + * (HiBy) drive their configfs gadget. */ void usb_set_audio(int value); + +/* true while host-PCM (USB-DAC) playback is active */ +bool usb_audio_get_active(void); #endif #if defined(USB_ENABLE_STORAGE) && defined(HAVE_MULTIDRIVE) diff --git a/uisimulator/common/stubs.c b/uisimulator/common/stubs.c index 9f9dc0d4e0..1823461709 100644 --- a/uisimulator/common/stubs.c +++ b/uisimulator/common/stubs.c @@ -202,6 +202,18 @@ bool usb_charging_enable(bool on) } #endif +#if defined(HAVE_HOST_USB_AUDIO) +void usb_set_audio(int value) +{ + (void)value; +} + +bool usb_audio_get_active(void) +{ + return false; +} +#endif + #ifdef HAVE_REMOTE_LCD_TICKING void lcd_remote_emireduce(bool state) { From dbd53ef8aec881c2085a6a5abfabdb8d2c488b24 Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Tue, 21 Jul 2026 00:38:53 +0100 Subject: [PATCH 026/408] hibyr1: implement USB DAC Implement the USB Audio Class DAC on the HiBy R1 and R3ProII, driven by the usb_audio setting from the scaffolding patch: the host plays audio over USB and the player's CS43131 renders it. The vendor kernel provides a UAC gadget function "uac_sa" whose char device /dev/uac_sa delivers the host's PCM: the isochronous OUT frames are converted to left-justified S16-in-S32 stereo and queued in a kernel ring, drained with a non-blocking read(). The data path lives in usb-dac-hiby.c -- a pump thread drains /dev/uac_sa into a small single-producer/single-consumer ring and the mixer callback hands that PCM to the codec through the normal ALSA output path. No resampling is needed because Rockbox clocks the codec at the host-negotiated rate. The DAC is built on the existing configfs gadget and binds the UDC like ADB, while usb_power_only keeps the USB thread out of the way. In charge mode it is a standalone self-bound gadget. In adb mode it is composed alongside adb on one gadget so the host gets a sound card and adb at once; uac_sa must be the first-linked function (audio interfaces 0-1) because the vendor function hard-codes its interface numbers. The pump is hardware-only, so usb-dac-hiby.c is excluded from simulator builds; the usb_audio_get_active() playback-interlock stub lives with the scaffolding. The R3ProII shares this hosted HiBy port and the same userspace gadget, and the data path is codec-agnostic, so nothing here is R1-specific. Only the R1 has been validated on-device. Co-Authored-By: Claude Opus 4.8 Change-Id: I64c57ede27f411a2c61d41a7e0fa127b51a7b5b9 --- docs/CREDITS | 1 + firmware/SOURCES | 4 + firmware/export/config/hibyr1.h | 1 + firmware/export/config/hibyr3proii.h | 1 + firmware/target/hosted/hiby/usb-dac-hiby.c | 214 ++++++++++++++ firmware/target/hosted/hiby/usb-dac-hiby.h | 34 +++ firmware/target/hosted/hiby/usb-hiby-gadget.c | 266 +++++++++++++++--- 7 files changed, 487 insertions(+), 34 deletions(-) create mode 100644 firmware/target/hosted/hiby/usb-dac-hiby.c create mode 100644 firmware/target/hosted/hiby/usb-dac-hiby.h diff --git a/docs/CREDITS b/docs/CREDITS index d1ac0cfdc7..d43382efb5 100644 --- a/docs/CREDITS +++ b/docs/CREDITS @@ -763,6 +763,7 @@ Eivind Ødegård Teun van Dalen Adam N. Burke Lê Khánh Bình +Michael McAllister The libmad team The wavpack team diff --git a/firmware/SOURCES b/firmware/SOURCES index 58bc753fc4..202575595f 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -187,6 +187,10 @@ target/hosted/hiby/hibylinux_codec.c #endif #endif +#if (defined(HIBY_R1) || defined(HIBY_R3PROII)) && !defined(SIMULATOR) +target/hosted/hiby/usb-dac-hiby.c +#endif + #if defined(AGPTEK_ROCKER) && !defined(SIMULATOR) target/hosted/agptek/button-agptek.c target/hosted/agptek/debug-agptek.c diff --git a/firmware/export/config/hibyr1.h b/firmware/export/config/hibyr1.h index f830791d9e..bb20b0b1a2 100644 --- a/firmware/export/config/hibyr1.h +++ b/firmware/export/config/hibyr1.h @@ -84,6 +84,7 @@ /* USB */ #define HAVE_USB_ADB +#define HAVE_HOST_USB_AUDIO #define HAVE_USB_POWER #define USB_VID_STR "32BB" #define USB_PID_STR "0101" diff --git a/firmware/export/config/hibyr3proii.h b/firmware/export/config/hibyr3proii.h index ffb0c69f16..92d338aee2 100644 --- a/firmware/export/config/hibyr3proii.h +++ b/firmware/export/config/hibyr3proii.h @@ -87,6 +87,7 @@ /* USB */ #define HAVE_USB_ADB +#define HAVE_HOST_USB_AUDIO #define HAVE_USB_POWER #define USB_VID_STR "32BB" #define USB_PID_STR "0101" diff --git a/firmware/target/hosted/hiby/usb-dac-hiby.c b/firmware/target/hosted/hiby/usb-dac-hiby.c new file mode 100644 index 0000000000..4b641a2314 --- /dev/null +++ b/firmware/target/hosted/hiby/usb-dac-hiby.c @@ -0,0 +1,214 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Michael McAllister + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* Host-PCM pump for the HiBy USB DAC gadget mode. + * + * The vendor kernel's UAC gadget function "uac_sa" exposes the char device + * /dev/uac_sa, which delivers the host's PCM: the isochronous OUT frames are + * converted to left-justified S16-in-S32 stereo and queued in a kernel ring, + * drained with read(). read() is non-blocking and returns -1 until at least + * the requested count (which must be <= the kernel ring/2) is buffered. + * + * A pump thread drains it into a small single-producer/single-consumer + * ring, the mixer's get_more callback hands that PCM to the CS43131 through + * the normal ALSA output path, the same way firmware/usbstack/usb_audio.c + * drives its host-PCM playback. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "kernel.h" +#include "pcm_mixer.h" +#include "pcm_sampr.h" +#include "system.h" +#include "thread.h" +#include "usb.h" +#include "usb-dac-hiby.h" + +/*#define LOGF_ENABLE*/ +#include "logf.h" + +#define UAC_SA_DEV "/dev/uac_sa" +/* ioctl: fills int[3] = { fmt, rate_hz, bits } */ +#define UAC_SA_GET_STATUS 1 +/* uac_sa delivers S32 stereo: 8 bytes per frame */ +#define UAC_SA_FRAME_SIZE 8 + +/* Ring of S16 stereo frames. A power of two so head/tail wrap cleanly. */ +#define DAC_RING_FRAMES 8192 /* ~186 ms at 44.1 kHz */ +#define DAC_CHUNK_FRAMES 512 /* mixer buffer granularity */ +/* one read()'s worth. Must stay <= the kernel ring/2 */ +#define DAC_READ_BYTES (DAC_CHUNK_FRAMES * UAC_SA_FRAME_SIZE) + +static int16_t dac_ring[DAC_RING_FRAMES * 2]; +static volatile unsigned int dac_head; /* frames produced; pump only */ +static volatile unsigned int dac_tail; /* frames consumed; mixer only */ +static pthread_t dac_thread; +static volatile bool dac_running; +static int dac_fd = -1; + +/* Mixer callback (PCM feed context). Returns the next chunk from the ring, + * zero-padded with silence on underrun so the stream never stalls. Double + * buffered so the returned pointer stays valid until the following call. */ +static void dac_get_more(const void **start, size_t *size) +{ + static int16_t out[2][DAC_CHUNK_FRAMES * 2]; + static int which; + int16_t *buf; + + which ^= 1; + buf = out[which]; + + unsigned int tail = dac_tail; + unsigned int avail = dac_head - tail; /* wrap-safe */ + avail = MIN(avail, DAC_CHUNK_FRAMES); + + for (unsigned int i = 0; i < avail; i++) + { + unsigned int idx = (tail + i) & (DAC_RING_FRAMES - 1); + buf[2 * i] = dac_ring[2 * idx]; + buf[2 * i + 1] = dac_ring[2 * idx + 1]; + } + memset(&buf[2 * avail], 0, + (DAC_CHUNK_FRAMES - avail) * 2 * sizeof(int16_t)); + dac_tail = tail + avail; + + *start = buf; + *size = DAC_CHUNK_FRAMES * 2 * sizeof(int16_t); +} + +/* Drain /dev/uac_sa into the ring. The driver's only file + * operations are open/read/ioctl (there is no poll()), so retry with a + * short sleep when the host has not queued a full chunk yet. + * + * This is a raw pthread, not a Rockbox thread, so it must use usleep() + * rather than the cooperative sleep() (which only the scheduler's own + * threads may call). */ +static void *dac_pump_thread(void *arg) +{ + int32_t rbuf[DAC_READ_BYTES / sizeof(int32_t)]; + (void)arg; + + while (dac_running) + { + unsigned int head = dac_head; + + /* Leave room for a full read; else wait for the mixer to drain */ + if (head - dac_tail > DAC_RING_FRAMES - DAC_CHUNK_FRAMES) + { + usleep(1000); + continue; + } + + ssize_t n = read(dac_fd, rbuf, DAC_READ_BYTES); + if (n <= 0) + { + usleep(1000); /* no host data queued yet */ + continue; + } + + int frames = n / UAC_SA_FRAME_SIZE; + for (int i = 0; i < frames; i++) + { + unsigned int idx = (head + i) & (DAC_RING_FRAMES - 1); + dac_ring[2 * idx] = (int16_t)(rbuf[2 * i] >> 16); + dac_ring[2 * idx + 1] = (int16_t)(rbuf[2 * i + 1] >> 16); + } + dac_head = head + frames; + } + + return NULL; +} + +bool usb_dac_start(void) +{ + static const struct mixer_play_cbs cbs = { .get_more = dac_get_more }; + int st[3] = {0, 0, 0}; + unsigned int rate; + + /* The node is created asynchronously when the UDC binds; during boot + * the hotplug helper can lag behind, so wait briefly for it. This runs + * on a Rockbox thread, so sleep() yields to the cooperative scheduler + * instead of stalling every other thread the way usleep() would. */ + for (int tries = 50; tries > 0; tries--) + { + dac_fd = open(UAC_SA_DEV, O_RDWR); + if (dac_fd >= 0) + break; + sleep(HZ / 50); /* 20 ms */ + } + if (dac_fd < 0) + { + logf("uac_sa open failed (%d)", errno); + return false; + } + + /* Play at the rate the host negotiated (reported by the kernel). */ + ioctl(dac_fd, UAC_SA_GET_STATUS, st); + if (st[1] >= SAMPR_8 && st[1] <= SAMPR_192) + rate = (unsigned int)st[1]; + else + rate = SAMPR_44; + mixer_set_frequency(rate); + + dac_head = dac_tail = 0; + dac_running = true; + if (pthread_create(&dac_thread, NULL, dac_pump_thread, NULL) != 0) + { + logf("uac_sa pump thread failed"); + dac_running = false; + close(dac_fd); + dac_fd = -1; + return false; + } + + mixer_channel_set_amplitude(PCM_MIXER_CHAN_USBAUDIO, MIX_AMP_UNITY); + mixer_channel_play_data(PCM_MIXER_CHAN_USBAUDIO, &cbs, NULL, 0); + return true; +} + +void usb_dac_stop(void) +{ + if (!dac_running) + return; + + dac_running = false; + pthread_join(dac_thread, NULL); + mixer_channel_stop(PCM_MIXER_CHAN_USBAUDIO); + + if (dac_fd >= 0) + { + close(dac_fd); + dac_fd = -1; + } +} + +bool usb_audio_get_active(void) +{ + return dac_running; +} diff --git a/firmware/target/hosted/hiby/usb-dac-hiby.h b/firmware/target/hosted/hiby/usb-dac-hiby.h new file mode 100644 index 0000000000..0e1c85d0a9 --- /dev/null +++ b/firmware/target/hosted/hiby/usb-dac-hiby.h @@ -0,0 +1,34 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Michael McAllister + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef __USB_DAC_HIBY_H__ +#define __USB_DAC_HIBY_H__ + +/** + * Host-PCM pump for the HiBy USB DAC gadget mode: drains /dev/uac_sa + * into the PCM mixer. Started/stopped by the USB gadget driver when the + * uac_sa function is brought up/torn down. + * + * usb_dac_start() returns false if the uac_sa device never appears (the + * pump is not started); the caller should fall back to a safe USB mode. + */ +bool usb_dac_start(void); +void usb_dac_stop(void); + +#endif /* __USB_DAC_HIBY_H__ */ diff --git a/firmware/target/hosted/hiby/usb-hiby-gadget.c b/firmware/target/hosted/hiby/usb-hiby-gadget.c index af04690248..5f9f055c0c 100644 --- a/firmware/target/hosted/hiby/usb-hiby-gadget.c +++ b/firmware/target/hosted/hiby/usb-hiby-gadget.c @@ -8,6 +8,7 @@ * * Copyright (C) 2018 by Marcin Bukat * Copyright (C) 2025 by Melissa Autumn + * Copyright (C) 2026 by Michael McAllister * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -20,6 +21,7 @@ ****************************************************************************/ #include +#include #include #include #include @@ -32,6 +34,12 @@ #include "power.h" #include "usb-hiby-gadget.h" +#include "audio.h" +#include "pcm.h" +#include "pcm_sampr.h" +#include "usb_ch9.h" +#include "usb-dac-hiby.h" + #ifdef HAVE_GENERAL_PURPOSE_LED #include "led-general-purpose.h" #endif @@ -40,14 +48,18 @@ #include "logf.h" #include +/* Configfs root of the gadget created by usb_init_device() */ +#define USB_GADGET_PATH "/sys/kernel/config/usb_gadget/adb_demo" + static int _usb_mode = -1; static bool _usb_init = false; static bool is_adb_running = false; +static int _usb_audio = 0; /* usb_audio setting: 0 never .. 3 while mass-storage */ void enable_adb(void); void enable_mass_storage(void); void enable_charging(void); -void enable_usb_audio(void); +bool enable_usb_audio(void); void disable_adb(void); void disable_mass_storage(void); @@ -58,16 +70,9 @@ void cleanup_rbhome(void); void startup_rbhome(void); #endif -void hiby_set_usb_mode(int mode) { - logf(">>>>>>>>>>>>>>>>> hiby_set_usb_mode(%d)\n", mode); - if (!_usb_init) { - logf("Need to init usb!\n"); - usb_init_device(); - } - - if (_usb_mode == mode) - return; - +/* Bring up the gadget for a base USB mode (mass storage / charge / adb). */ +static void configure_usb_mode(int mode) +{ switch(mode) { case USB_MODE_MASS_STORAGE: logf("Enabling Mass Storage\n"); @@ -84,8 +89,104 @@ void hiby_set_usb_mode(int mode) { default: break; } +} +/* Whether the USB-DAC function should be running, given the usb_audio + * setting (never/always/while_charge_only/while_mass_storage) and the + * current base USB mode. */ +static bool dac_should_run(void) +{ + switch (_usb_audio) { + case 1: return true; /* always */ + case 2: return _usb_mode == USB_MODE_CHARGE; /* while charge-only */ + case 3: return _usb_mode == USB_MODE_MASS_STORAGE; /* while mass-storage */ + default: return false; /* never */ + } +} + +/* Reconcile the DAC gadget with the setting + current mode. + * + * In adb mode the DAC rides alongside adb as a composite function, so we + * rebuild the adb gadget (enable_adb() composes uac_sa when dac_should_run()). + * In charge/mass-storage mode the DAC is a standalone self-bound gadget. */ +static void apply_usb_audio(void) +{ + if (_usb_mode == USB_MODE_ADB) { + enable_adb(); + return; + } + + bool want = dac_should_run(); + bool active = usb_audio_get_active(); + + if (want && !active) { + /* enable_usb_audio() returns false if applied before audio_init() + * (PCM mixer not up yet -- e.g. at boot) or if uac_sa never comes + * up. Either way restore the base mode so we never leave a dead + * gadget behind. */ + if (!enable_usb_audio()) + configure_usb_mode(_usb_mode); + } else if (!want && active) { + disable_usb_audio(); + configure_usb_mode(_usb_mode); + } +} + +/* usb_set_audio() is the target's implementation of the usb_audio setting + * callback declared in usb.h (HAVE_HOST_USB_AUDIO); it drives the DAC gadget + * up or down. */ +void usb_set_audio(int value) +{ + _usb_audio = value; + apply_usb_audio(); +} + +/* Compose uac_sa into the adb config, linked FIRST so the audio interfaces + * are 0-1 (the vendor uac_sa function hard-codes them), with the DAC VID/PID + * + IAD descriptor. Returns true if it composed the DAC. Skipped before + * audio_init() (no PCM mixer yet); re-applied once it is up. */ +static bool adb_compose_dac(void) +{ + if (!dac_should_run() || !pcm_is_initialized()) + return false; + + sysfs_set_string(USB_GADGET_PATH "/idVendor", "0x" USB_VID_STR); + sysfs_set_string(USB_GADGET_PATH "/idProduct", "0x0004"); + sysfs_set_int(USB_GADGET_PATH "/bDeviceClass", USB_CLASS_MISC); + sysfs_set_int(USB_GADGET_PATH "/bDeviceSubClass", 2); + sysfs_set_int(USB_GADGET_PATH "/bDeviceProtocol", 1); + + system("mkdir -p " USB_GADGET_PATH "/functions/uac_sa.a"); + sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_chmask", 3); + sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_ssize", 2); + sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_srate", SAMPR_48); + system("ln -sf " USB_GADGET_PATH "/functions/uac_sa.a " USB_GADGET_PATH "/configs/c.1/"); + sysfs_set_string(USB_GADGET_PATH "/configs/c.1/strings/0x409/configuration", "uac,adb"); + return true; +} + +void hiby_set_usb_mode(int mode) { + logf(">>>>>>>>>>>>>>>>> hiby_set_usb_mode(%d)\n", mode); + if (!_usb_init) { + logf("Need to init usb!\n"); + usb_init_device(); + } + + if (_usb_mode == mode) + return; + + /* Drop any active DAC before reconfiguring the base gadget. */ + if (usb_audio_get_active()) + disable_usb_audio(); + + /* Set the mode first so enable_adb()/dac_should_run() see it. */ _usb_mode = mode; + configure_usb_mode(mode); + + /* adb composes the DAC itself in enable_adb(); charge/mass-storage get + * the standalone self-bound DAC here. */ + if (mode != USB_MODE_ADB) + apply_usb_audio(); } /* TODO: implement usb detection properly */ @@ -113,7 +214,9 @@ void usb_enable(bool on) logf(">>>>>>>>>>>>>>>>> usb_enable(%d)\n", on); logf("usb enable %d %d\n", on, _usb_mode); - if (_usb_mode == USB_MODE_ADB) + /* ADB binds the UDC itself, and so does the USB-DAC while it is active, + * so leave the UDC alone in those cases. */ + if (_usb_mode == USB_MODE_ADB || usb_audio_get_active()) return; // Re-arm the LUN on each connect, otherwise the disk is only exported the first time. @@ -164,35 +267,51 @@ void enable_charging(void) { disable_mass_storage(); disable_adb(); + disable_usb_audio(); } void enable_adb(void) { logf(">>>>>>>>>>>>>>>>> set_adb()\n"); - // Disable mass storage if it was running - disable_mass_storage(); - - // Remove any lingering adb daemon and its respawner + // Stop the respawner FIRST so it can't rebind adbd under us. system("killall adbserver.sh adbd 2>/dev/null"); - system("mkdir -p /sys/kernel/config/usb_gadget/adb_demo/configs/c.1/strings/0x409"); - system("mkdir -p /sys/kernel/config/usb_gadget/adb_demo/functions/ffs.adb"); + /* Disable mass storage and USB DAC if they were running (unbinds the UDC + * and clears any uac_sa link/descriptor). */ + disable_mass_storage(); + disable_usb_audio(); - // Use the adb VID/PID so the host recognises the device - sysfs_set_string("/sys/kernel/config/usb_gadget/adb_demo/idVendor", "0x18d1"); - sysfs_set_string("/sys/kernel/config/usb_gadget/adb_demo/idProduct", "0xd002"); + /* Also drop the adb function link so we control the link order below + * (uac_sa must be linked before adb -- see adb_compose_dac()). */ + if (access(USB_GADGET_PATH "/configs/c.1/ffs.adb", F_OK) == 0) + system("rm " USB_GADGET_PATH "/configs/c.1/ffs.adb"); - // Now we'll override configuration and MaxPower - sysfs_set_string("/sys/kernel/config/usb_gadget/adb_demo/configs/c.1/strings/0x409/configuration", "adb"); - sysfs_set_int("/sys/kernel/config/usb_gadget/adb_demo/configs/c.1/MaxPower", 120); + system("mkdir -p " USB_GADGET_PATH "/configs/c.1/strings/0x409"); + system("mkdir -p " USB_GADGET_PATH "/functions/ffs.adb"); - // And link up the adb function to the usb gadget config - system("ln -sf /sys/kernel/config/usb_gadget/adb_demo/functions/ffs.adb /sys/kernel/config/usb_gadget/adb_demo/configs/c.1/"); + /* Compose the USB-DAC (uac_sa, linked first) alongside adb when the + * usb_audio setting wants it; otherwise present plain adb. */ + bool with_dac = adb_compose_dac(); + if (!with_dac) { + // Use the adb VID/PID so the host recognises the device + sysfs_set_string(USB_GADGET_PATH "/idVendor", "0x18d1"); + sysfs_set_string(USB_GADGET_PATH "/idProduct", "0xd002"); + sysfs_set_string(USB_GADGET_PATH "/configs/c.1/strings/0x409/configuration", "adb"); + } + + sysfs_set_int(USB_GADGET_PATH "/configs/c.1/MaxPower", 120); + + // Link adb into the config (after uac_sa, so adb is the later interface) + system("ln -sf " USB_GADGET_PATH "/functions/ffs.adb " USB_GADGET_PATH "/configs/c.1/"); system("mkdir -p /dev/usb-ffs/adb"); // Let the vendor respawner mount functionfs and run adbd, which binds the UDC. system("/sbin/adbserver.sh 440 &"); + + /* uac_sa is bound by the same auto-bind; start pumping /dev/uac_sa. */ + if (with_dac) + usb_dac_start(); } @@ -230,8 +349,9 @@ void disable_adb(void) { void enable_mass_storage(void) { logf(">>>>>>>>>>>>>>>>> set_mass_storage()\n"); - // Disable adb if it's running + /* Disable adb and USB DAC if they're running */ disable_adb(); + disable_usb_audio(); system("mkdir -p /sys/kernel/config/usb_gadget/adb_demo/functions/mass_storage.0/lun.0"); system("mkdir -p /sys/kernel/config/usb_gadget/adb_demo/configs/c.1/strings/0x409"); @@ -270,18 +390,96 @@ void disable_mass_storage(void) { } } -#if defined(HAVE_USB_AUDIO) -void enable_usb_audio(void) { +/* USB DAC (USB Audio Class gadget). + * + * The vendor kernel provides a UAC function "uac_sa"; building it into the + * gadget and binding the UDC creates /dev/uac_sa, which usb-dac-hiby.c pumps + * into the PCM mixer. enable_usb_audio() is the standalone path used in + * charge/mass-storage mode; adb mode composes uac_sa alongside adb in + * enable_adb() instead. */ +bool enable_usb_audio(void) +{ + logf(">>>>>>>>>>>>>>>>> enable_usb_audio()\n"); + + /* The DAC pump feeds the PCM mixer, which isn't up until audio_init(), so + * a DAC setting applied at boot lands too early. Bail before touching + * anything so the caller can restore the base mode. */ + if (!pcm_is_initialized()) + return false; + + /* The DAC hands the output path to the host's PCM; stop local playback + * first (it can't restart while the DAC is active). */ + audio_stop(); + + /* Standalone DAC: tear down the other gadget functions first. */ + disable_mass_storage(); + disable_adb(); + + /* Build the "uac_sa" function: stereo, 16-bit, capture (host->device). + * Set all three explicitly: the kernel's defaults are 3/2/64000, so at + * least the 64 kHz capture rate must always be overridden. */ + system("mkdir -p " USB_GADGET_PATH "/functions/uac_sa.a"); + sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_chmask", 3); + sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_ssize", 2); + sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_srate", SAMPR_48); + + /* Present an IAD audio device with the HiBy DAC VID/PID so the host + * recognises it as a USB DAC. */ + sysfs_set_int(USB_GADGET_PATH "/bDeviceClass", USB_CLASS_MISC); + sysfs_set_int(USB_GADGET_PATH "/bDeviceSubClass", 2); + sysfs_set_int(USB_GADGET_PATH "/bDeviceProtocol", 1); + sysfs_set_string(USB_GADGET_PATH "/idVendor", "0x" USB_VID_STR); + sysfs_set_string(USB_GADGET_PATH "/idProduct", "0x0004"); + + system("mkdir -p " USB_GADGET_PATH "/configs/c.1/strings/0x409"); + sysfs_set_string(USB_GADGET_PATH + "/configs/c.1/strings/0x409/configuration", "uac"); + sysfs_set_int(USB_GADGET_PATH "/configs/c.1/bmAttributes", + USB_CONFIG_ATT_ONE | USB_CONFIG_ATT_SELFPOWER); + sysfs_set_int(USB_GADGET_PATH "/configs/c.1/MaxPower", 120); + + system("ln -sf " USB_GADGET_PATH "/functions/uac_sa.a " + USB_GADGET_PATH "/configs/c.1/"); + + /* Bind the UDC ourselves -- in charge mode the usb thread is power-only, + * and usb_enable() bails while the DAC is active, so nothing else binds + * it. This creates /dev/uac_sa. */ + sysfs_set_string(USB_GADGET_PATH "/UDC", "13500000.otg_new"); + + return usb_dac_start(); } -void disable_usb_audio(void) { +void disable_usb_audio(void) +{ + logf(">>>>>>>>>>>>>>>>> disable_usb_audio()\n"); + usb_dac_stop(); + + /* Unbind the UDC and tear the function down so the next mode starts + * from a clean gadget. */ + if (access(USB_GADGET_PATH "/UDC", F_OK) == 0) + sysfs_set_string(USB_GADGET_PATH "/UDC", "\n"); + + if (access(USB_GADGET_PATH "/configs/c.1/uac_sa.a", F_OK) == 0) + system("rm " USB_GADGET_PATH "/configs/c.1/uac_sa.a"); + + if (access(USB_GADGET_PATH "/functions/uac_sa.a", F_OK) == 0) + system("rmdir " USB_GADGET_PATH "/functions/uac_sa.a"); + + /* Restore a plain (non-IAD) device descriptor and the target's own + * VID/PID for the other modes. */ + sysfs_set_int(USB_GADGET_PATH "/bDeviceClass", USB_CLASS_PER_INTERFACE); + sysfs_set_int(USB_GADGET_PATH "/bDeviceSubClass", 0); + sysfs_set_int(USB_GADGET_PATH "/bDeviceProtocol", 0); + sysfs_set_string(USB_GADGET_PATH "/idVendor", "0x" USB_VID_STR); + sysfs_set_string(USB_GADGET_PATH "/idProduct", "0x" USB_PID_STR); + + if (access(USB_GADGET_PATH + "/configs/c.1/strings/0x409/configuration", F_OK) == 0) + sysfs_set_string(USB_GADGET_PATH + "/configs/c.1/strings/0x409/configuration", ""); } -#else -void enable_usb_audio(void) {} -void disable_usb_audio(void) {} -#endif void usb_init_device(void) { From 958e84c042fe8f0392b89317711fcc73bd573e64 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 23 Jul 2026 19:41:40 +0300 Subject: [PATCH 027/408] codec: flac: fix playback of certain files Use flac_seek by time even when elapsedtime is 0, and apply it as a fallback for failed offset seeks since it provides more robust error recovery. Change-Id: I438888fba02bda38137f3f1347bb1f657ef9c166 --- lib/rbcodec/codecs/flac.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/rbcodec/codecs/flac.c b/lib/rbcodec/codecs/flac.c index 0a9d535c3e..00d4dd71ca 100644 --- a/lib/rbcodec/codecs/flac.c +++ b/lib/rbcodec/codecs/flac.c @@ -490,9 +490,7 @@ enum codec_status codec_run(void) ci->configure(DSP_SET_STEREO_MODE, fc.channels == 1 ? STEREO_MONO : STEREO_NONINTERLEAVED); codec_set_replaygain(ci->id3); - - if (samplesdone || !elapsedtime) { - flac_seek_offset(&fc, samplesdone); + if (samplesdone && flac_seek_offset(&fc, samplesdone)) { samplesdone=fc.samplenumber+fc.blocksize; elapsedtime=((uint64_t)samplesdone*1000)/(ci->id3->frequency); } From 8d84986fc086503a021d5f6d4b1e21f39b22db5d Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Tue, 9 Jun 2026 18:37:54 +0300 Subject: [PATCH 028/408] pcm_mixer: fix active_channels handling in mixer_handle_sampr_change `channel_stopped` compacts the `active_channels` array, so we should check the same index for the next active channel. Change-Id: I54c42f9b6c97f0c54d43b8faae0052e856f3c06d --- firmware/pcm_mixer.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/firmware/pcm_mixer.c b/firmware/pcm_mixer.c index 5de15ac793..7c17255990 100644 --- a/firmware/pcm_mixer.c +++ b/firmware/pcm_mixer.c @@ -288,10 +288,11 @@ static void mixer_start_pcm(void) /* Notify users of samplerate change */ static void mixer_handle_sampr_change(unsigned int sampr) { - for (size_t i = 0; i < ARRAYLEN(active_channels) && active_channels[i]; i += 1) + int i = 0; + struct mixer_channel* chan; + while ((chan = active_channels[i])) { - struct mixer_channel* chan = active_channels[i]; - + bool stop_channel = true; /* Notify upstreams */ if (chan->play_cbs) { @@ -309,13 +310,19 @@ static void mixer_handle_sampr_change(unsigned int sampr) chan->start = start; chan->size = size; chan->last_size = 0; - } else { - channel_stopped(chan); + + stop_channel = false; + ++i; } } } + + if (stop_channel) + { + channel_stopped(chan); + } /* Notify buffer monitor */ - if (chan->buf_cbs) + else if (chan->buf_cbs) { if (chan->buf_cbs->sampr_changed) { From 24c377914678a173ea1a6187c43baa026a88afe7 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Fri, 24 Jul 2026 12:40:22 -0400 Subject: [PATCH 029/408] rbsettings.lua add routines to write settings supports most settings and status items allows writing rockbox settings from lua Change-Id: I852489fd9819c8f7147c922e1d03c4ed9ebafa53 --- apps/plugins/lua/include_lua/rbsettings.lua | 124 +++++++++++++++++++- 1 file changed, 122 insertions(+), 2 deletions(-) diff --git a/apps/plugins/lua/include_lua/rbsettings.lua b/apps/plugins/lua/include_lua/rbsettings.lua index 4da6ee61b2..7f9d00982f 100644 --- a/apps/plugins/lua/include_lua/rbsettings.lua +++ b/apps/plugins/lua/include_lua/rbsettings.lua @@ -1,4 +1,4 @@ ---[[ Lua rb settings reader +--[[ Lua rb settings reader / writer /*************************************************************************** * __________ __ ___. * Open \______ \ ____ ____ | | _\_ |__ _______ ___ @@ -7,7 +7,7 @@ * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * - * Copyright (C) 2019 William Wilgus + * Copyright (C) 2026 William Wilgus * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -18,6 +18,33 @@ * KIND, either express or implied. * ****************************************************************************/ +see https://forums.rockbox.org/index.php/topic,55820.0.html for an example +see rocklib.c for definitive list of supported settings +rb.global_settings() +rb.global_status() +rb.audio_next_track() +rb.audio_current_track() + +NOTES: +Not all settings are accessible from lua, for instance you can read char_ptrs +but writing them is not supported since we cannot guarantee the lifetime once +lua terminates, or settigns containing unions since its not possible to discern +what the value is at compile time these items will be an opaque pointer to the +structure in memory but lua does not know how to decode it + +If you are writing settings that you want to persist do not forget to call +rb.settings_save() though not calling it does NOT guarantee a changed setting +won't be written behind your back so be sure to save the old value +and restore it when you are finished and call rb.settings_save() +to ensure the old value gets saved + +To Write stucts and arrays it is recommended to first make a call to +newval = settings_read('global_settings', 'setting.foo', 'system') +and make changes to the returned table + newval.foo = bar +followed by + setting_writesettings_read('global_settings', 'rb.foo', 'system', newval) +with the altered table. ]] rb.settings = rb.settings or {} @@ -47,6 +74,7 @@ end local function format_val(val, var_type) local ret, num + if var_type == nil then return nil elseif var_type == "str" then @@ -71,6 +99,9 @@ local function format_val(val, var_type) return ret end +--[[ ex. dump_struct(rb.global_settings, + rb.system.replaygain_settings, 0, [0x2c, 12, s_replaygain_settings])]] + local function dump_struct(t_settings, t_struct, n_elems, t_var) --Internal function dumps structs local tdata = {} @@ -101,6 +132,44 @@ local function dump_struct(t_settings, t_struct, n_elems, t_var) return tdata end +--[[ ex. write_struct(rb.global_settings, + rb.system.eq_band_setting, 10, [ 0x54, 120, s_eq_band_setting[10] ])]] +local function write_struct(t_settings, t_struct, n_elems, t_var, tdata) + --Internal function writes structs + local function struct_write_elem(v, elem_offset, newval) + local offset, tvar1 + tvar1 = get_var_fields(v) + offset = t_var[var.offset] + tvar1[var.offset] + elem_offset + return t_settings(offset, tvar1[var.size], newval) == 1 + end + + if n_elems > 0 then + -- Array of structs, struct[elems]; + local elemsize = (t_var[var.size] / n_elems) + for i = 0, n_elems - 1 do + tdata[i] = tdata[i] or {} + for k1, v1 in pairs(t_struct) do + --tdata[i][k1] = struct_get_elem(v1, (elemsize * i)) + if tdata[i][k1] ~= nil then + if not struct_write_elem(v1, (elemsize * i), tdata[i][k1]) then + return false + end + end + end + end + else + -- single struct, struct; + for k1, v1 in pairs(t_struct) do + if tdata[k1] ~= nil then + if not struct_write_elem(v1, 0, tdata[k1]) then + return false + end + end + end + end + return true +end + local function get_array_elems(var_type) --extract the number of elements, returns 0 if not found local elems = string.match(var_type,".*%[(%d+)%]") @@ -150,6 +219,57 @@ function rb.settings.read(s_settings, s_var, s_groupname) return data end +function rb.settings.write(s_settings, s_var, s_groupname, newval) + local data, val + local res = 0 + local tvar = get_var_fields(s_var) + if tvar == nil then return nil end + + local elems = get_array_elems(tvar[var.type]) + local structname = get_struct_name(tvar[var.type]) + + local tsettings = rb[s_settings] + if not tsettings then error(s_settings .. " does not exist") end + if not newval then error(s_settings .. " nothing to write") end + + if structname and rb[s_groupname] then + -- rb.splash(300, "struct " .. structname .. " - " .. s_groupname) + if type (newval) ~= "table" then + error("newval table expected got " .. type(newval)) + end + if not write_struct(tsettings, rb[s_groupname][structname], elems, tvar, newval) then + error(s_groupname .. " writing failed") + end + return 1 + end + + local voffset, vsize, vtype = tvar[var.offset], tvar[var.size], tvar[var.type] + if elems > 0 then + if type (newval) ~= "table" then + error("newval table expected got " .. type(newval)) + end + -- Arrays of values, val[elems]; + local elemsize = (vsize / elems) + + for i = 0, elems - 1 do + if newval[i] ~= nil then + if tsettings(voffset + (elemsize * i), elemsize, newval[i]) ~= 1 then + res = 0 + break + end + end + end + else + -- Single value, val; + if vtype == "ptr_char" then -- (**char) + error(s_var .. " is a string pointer") + else + res = tsettings(voffset, vsize, newval) + end + end + return res +end + function rb.settings.dump(s_settings, s_groupname, s_structname, t_output, fn_filter) t_output = t_output or {} fn_filter = fn_filter or function(s,k) return true end From 4f6aac445f8e32cb4c721032dc2acb47c693fb13 Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Sun, 26 Jul 2026 20:57:02 +0100 Subject: [PATCH 030/408] hiby: raise the plugin buffer to 2MiB on the 64MB targets PictureFlow refuses to start on the larger panels in this family with "Not enough memory for album art cache", no matter how small the library is. It is PF_PLAYBACK_CAPABLE here, so it deliberately takes the plugin buffer instead of stealing the audio buffer in order to keep playback running. It then gives a quarter of that buffer to its album art cache, which needs DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(pix_t) bytes: HiBy R1 480x800 -> 400x533 -> 416 KiB HiBy R3 Pro II 480x720 -> 360x480 -> 338 KiB Surfans F28 320x480 -> 240x320 -> 150 KiB A quarter of 512KiB is only 128KiB, so all three fail the check before they ever look at the library. The rest of the family tops out at 320x240 and needs at most 68KiB, which is why the old value went unnoticed. Most hibyos devices only have 32MB of RAM in total, and the OS and its daemons eat about two thirds of that, so there is nothing spare to hand out and the buffer stays at 512KiB. The three affected targets happen to be exactly the 64MB ones, and they are also the only members of the family configured with MEMORYSIZE >= 16, so key the size off that. Plugins are dlopen()ed on hosted targets, so pluginbuf is plain BSS and plugin.lds/DRAMSIZE do not apply. Change-Id: I38db01231bbb8d139cb2239623a79250e0e46b61 --- firmware/export/config/hibylinux.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/firmware/export/config/hibylinux.h b/firmware/export/config/hibylinux.h index 98e8893a41..1b2e2deee4 100644 --- a/firmware/export/config/hibylinux.h +++ b/firmware/export/config/hibylinux.h @@ -39,7 +39,11 @@ #define CODEC_SIZE 0x100000 /* The number of bytes reserved for loadable plugins */ -#define PLUGIN_BUFFER_SIZE 0x80000 +#if MEMORYSIZE >= 16 +#define PLUGIN_BUFFER_SIZE 0x200000 /* 2 MiB */ +#else +#define PLUGIN_BUFFER_SIZE 0x80000 /* 512 KiB */ +#endif #define CONFIG_LCD LCD_INGENIC_LINUX From 5f129ef29936be3f0145f3efabf6e14098d9abc5 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Mon, 27 Jul 2026 20:24:57 -0400 Subject: [PATCH 031/408] tools: Update mkinfo to handle the echor1 target's symbols With this change, RAM size is computed (and reported) properly. Change-Id: I4ead8f677c69b78141b137e05d9da942dd4d8138 --- tools/mkinfo.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/mkinfo.pl b/tools/mkinfo.pl index 463a10005e..0e85b57415 100755 --- a/tools/mkinfo.pl +++ b/tools/mkinfo.pl @@ -48,11 +48,11 @@ sub mapscan { my $start, $end; open(M, "<$f"); while() { - if($_ =~ / +0x([0-9a-f]+) *_end = \./) { + if($_ =~ / +0x([0-9a-f]+) *_(bss)?end = \./) { $end = $1; last; } - elsif($_ =~ / +0x([0-9a-f]+) *_loadaddress = \./) { + elsif($_ =~ / +0x([0-9a-f]+) *_?loadaddress = \./) { $start = $1; } elsif($_ =~ / +0x([0-9a-f]+) *_dramcopystart = \./) { From 31dfd5da2e6d3522c6db027c99485314c87cc20e Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Sun, 26 Jul 2026 08:52:15 +0300 Subject: [PATCH 032/408] playback: add Playlist Single Mode option Change-Id: I27c782ad246135fa6c98b52ed1491e86a7dfd456 --- apps/playback.c | 3 +++ apps/settings.h | 3 ++- apps/settings_list.c | 8 +++++--- manual/appendix/config_file_options.tex | 2 +- manual/configure_rockbox/playback_options.tex | 1 + 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/playback.c b/apps/playback.c index ccb9aa1c8c..270de50bf6 100644 --- a/apps/playback.c +++ b/apps/playback.c @@ -2685,6 +2685,9 @@ static bool single_mode_do_pause(int id3_hid) if (global_settings.single_mode == SINGLE_MODE_TRACK) return true; + if (global_settings.single_mode == SINGLE_MODE_PLAYLIST && skip_pending == TRACK_SKIP_AUTO_NEW_PLAYLIST) + return true; + char *previous_tag = single_mode_get_id3_tag(id3_get(PLAYING_ID3)); char *new_tag = single_mode_get_id3_tag(bufgetid3(id3_hid)); return previous_tag == NULL || diff --git a/apps/settings.h b/apps/settings.h index a2ce824a5e..0fc07a2413 100644 --- a/apps/settings.h +++ b/apps/settings.h @@ -124,7 +124,8 @@ enum { SINGLE_MODE_ARTIST, SINGLE_MODE_COMPOSER, SINGLE_MODE_GROUPING, - SINGLE_MODE_GENRE + SINGLE_MODE_GENRE, + SINGLE_MODE_PLAYLIST, }; enum diff --git a/apps/settings_list.c b/apps/settings_list.c index 0887e0527a..42d4dc4245 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -1471,8 +1471,8 @@ const struct settings_list settings[] = { OFFON_SETTING(0,play_selected,LANG_PLAY_SELECTED,true,"play selected",NULL), CHOICE_SETTING(0, single_mode, LANG_SINGLE_MODE, 0, "single mode", - "off,track,album,album artist,artist,composer,work,genre", - NULL, 8, + "off,track,album,album artist,artist,composer,work,genre,playlist", + NULL, 9, ID2P(LANG_OFF), ID2P(LANG_TRACK), ID2P(LANG_ID3_ALBUM), @@ -1480,7 +1480,9 @@ const struct settings_list settings[] = { ID2P(LANG_ID3_ARTIST), ID2P(LANG_ID3_COMPOSER), ID2P(LANG_ID3_GROUPING), - ID2P(LANG_ID3_GENRE)), + ID2P(LANG_ID3_GENRE), + ID2P(LANG_PLAYLIST) + ), OFFON_SETTING(0,party_mode,LANG_PARTY_MODE,false,"party mode",NULL), OFFON_SETTING(0,fade_on_stop,LANG_FADE_ON_STOP,true,"volume fade",NULL), INT_SETTING(F_TIME_SETTING, ff_rewind_min_step, LANG_FFRW_STEP, 1, diff --git a/manual/appendix/config_file_options.tex b/manual/appendix/config_file_options.tex index 2b580f3206..d7adb64be9 100644 --- a/manual/appendix/config_file_options.tex +++ b/manual/appendix/config_file_options.tex @@ -36,7 +36,7 @@ repeat & off, all, one, shuffle, ab & N/A\\ play selected & on, off & N/A\\ - single mode & off, track, album, album artist, artist, composer, work, genre + single mode & off, track, album, album artist, artist, composer, work, genre, playlist & N/A\\ party mode & on, off & N/A\\ scan min step & 1, 2, 3, 4, 5, 6, 8, 10, 15, 20, 25, 30, 45, 60 diff --git a/manual/configure_rockbox/playback_options.tex b/manual/configure_rockbox/playback_options.tex index 3287c61739..66a90c6f6b 100644 --- a/manual/configure_rockbox/playback_options.tex +++ b/manual/configure_rockbox/playback_options.tex @@ -88,6 +88,7 @@ you to configure settings related to audio playback. \setting{Composer}, \setting{Work}, or \setting{Genre}, your music will pause at the next track change where this tag also changes. + If it is set to Playlist, your music will pause on the next automatic playlist change. \section{Party Mode} Enables unstoppable music playback. When new songs are From c54dddc2ac84285be801b5880230c126cb27a68a Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Tue, 28 Jul 2026 19:53:45 +0200 Subject: [PATCH 033/408] playback: fix Playlist Single Mode pause behavior (31dfd5da2e) Don't pause after every track when playlist hasn't changed Change-Id: Ic4f0261bb96ac1883c8d5857d0dd9db47b2e2c69 --- apps/playback.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/playback.c b/apps/playback.c index 270de50bf6..29f06dcb69 100644 --- a/apps/playback.c +++ b/apps/playback.c @@ -2685,8 +2685,8 @@ static bool single_mode_do_pause(int id3_hid) if (global_settings.single_mode == SINGLE_MODE_TRACK) return true; - if (global_settings.single_mode == SINGLE_MODE_PLAYLIST && skip_pending == TRACK_SKIP_AUTO_NEW_PLAYLIST) - return true; + if (global_settings.single_mode == SINGLE_MODE_PLAYLIST) + return (skip_pending == TRACK_SKIP_AUTO_NEW_PLAYLIST); char *previous_tag = single_mode_get_id3_tag(id3_get(PLAYING_ID3)); char *new_tag = single_mode_get_id3_tag(bufgetid3(id3_hid)); From 58d4d2b221f9800445f330b94e64577a4265211d Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Wed, 29 Jul 2026 10:27:35 -0400 Subject: [PATCH 034/408] desktop: Drop the 'version' fields They refer to the *desktop file specification* not the application version Change-Id: Ib35f460c73aae40961255826f0c767a22cb5b518 --- utils/rbutilqt/RockboxUtility.desktop | 1 - utils/themeeditor/RockboxThemeEditor.desktop | 1 - 2 files changed, 2 deletions(-) diff --git a/utils/rbutilqt/RockboxUtility.desktop b/utils/rbutilqt/RockboxUtility.desktop index cd0fc08cfc..0ffe1b28bb 100644 --- a/utils/rbutilqt/RockboxUtility.desktop +++ b/utils/rbutilqt/RockboxUtility.desktop @@ -1,6 +1,5 @@ [Desktop Entry] GenericName=Rockbox Installer and Maintainance Tool -Version=1.5.2 Hidden=false Name=Rockbox Utility Type=Application diff --git a/utils/themeeditor/RockboxThemeEditor.desktop b/utils/themeeditor/RockboxThemeEditor.desktop index a8a731fcf9..818bc60947 100644 --- a/utils/themeeditor/RockboxThemeEditor.desktop +++ b/utils/themeeditor/RockboxThemeEditor.desktop @@ -1,6 +1,5 @@ [Desktop Entry] GenericName=Rockbox Theme Editor -Version=1.0 Hidden=false Name=Rockbox Theme Editor Type=Application From d42dcdcba22c1046cbee99154b3ed3ac9c7a6a39 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Wed, 29 Jul 2026 22:07:38 +0300 Subject: [PATCH 035/408] rbutilqt/themeeditor: Allow specifying the Apple code signing ID using an environment variable Co-authored-by: ChatGPT-5.5 Change-Id: Idd4624c7851f7fa37afac4de59f319e0087d0bc6 --- utils/cmake/deploy.cmake | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/utils/cmake/deploy.cmake b/utils/cmake/deploy.cmake index ff21da4444..cfbae24d50 100644 --- a/utils/cmake/deploy.cmake +++ b/utils/cmake/deploy.cmake @@ -96,6 +96,17 @@ endif() if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") set(CODESIGN_IDENTITY "-" CACHE STRING "Code signing identity to use for macOS signing") + + if(DEFINED ENV{CODESIGN_IDENTITY}) + set(CODESIGN_IDENTITY "$ENV{CODESIGN_IDENTITY}") + endif() + + if("${CODESIGN_IDENTITY}" STREQUAL "-") + message("-- Using ad-hoc Apple code signing") + else() + message("-- Using Apple code signing identity: ${CODESIGN_IDENTITY}") + endif() + set(DMGBUILD_STAMP ${CMAKE_BINARY_DIR}/dmgbuild.stamp) add_custom_command( COMMENT "Setting up dmgbuild virtualenv" From 3af4e207920c832ec0248c85e84b56a7d49e96ff Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Wed, 29 Jul 2026 05:57:17 +0200 Subject: [PATCH 036/408] skin_engine: fix div by 0 for %pP tag Fixes possible crash during playback with 'Auto-Change Directory' setting enabled. When the currently playing directory changes, the skin may be updated on the main thread while the playback thread is executing create_and_play_dir and has yet to add any indices to the newly created playlist. Introduced in commit 41d5ca3. Change-Id: I6e930f2db546c7690b362ba8c1a4907d0348ff52 --- apps/gui/skin_engine/skin_display.c | 10 +++------- apps/gui/skin_engine/skin_tokens.c | 2 ++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/gui/skin_engine/skin_display.c b/apps/gui/skin_engine/skin_display.c index c2c19b2b8c..abeeef16ff 100644 --- a/apps/gui/skin_engine/skin_display.c +++ b/apps/gui/skin_engine/skin_display.c @@ -178,7 +178,7 @@ void draw_progressbar(struct gui_wps *gwps, struct skin_viewport* skin_viewport, struct wps_state *state = get_wps_state(); struct mp3entry *id3 = state->id3; int x = pb->x, y = pb->y, width = pb->width, height = pb->height; - unsigned long length, end; + unsigned long end, length = 0; int flags = HORIZONTAL; if (height < 0) @@ -221,11 +221,6 @@ void draw_progressbar(struct gui_wps *gwps, struct skin_viewport* skin_viewport, length = pl_total; end = pl_elapsed; } - else - { - length = 1; - end = 0; - } } else if (pb->type == SKIN_TOKEN_PEAKMETER_LEFTBAR || pb->type == SKIN_TOKEN_PEAKMETER_RIGHTBAR) @@ -281,7 +276,8 @@ void draw_progressbar(struct gui_wps *gwps, struct skin_viewport* skin_viewport, length = id3->length; end = id3->elapsed + state->ff_rewind_count; } - else + + if (length <= 0) { length = 1; end = 0; diff --git a/apps/gui/skin_engine/skin_tokens.c b/apps/gui/skin_engine/skin_tokens.c index 32daf5dc00..cab4385bb9 100644 --- a/apps/gui/skin_engine/skin_tokens.c +++ b/apps/gui/skin_engine/skin_tokens.c @@ -1354,6 +1354,8 @@ const char *get_token_value(struct gui_wps *gwps, case SKIN_TOKEN_PLAYLIST_PERCENT: { int playlist_amt = playlist_amount(); + if (playlist_amt <= 0) + return NULL; int current_pos = playlist_get_display_index() + offset; int percentage = current_pos * 100 / playlist_amt; From db87622e7d5dad53af6fad8b29c81561b0b71646 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Thu, 30 Jul 2026 12:44:58 +0200 Subject: [PATCH 037/408] fix yellow in 3af4e20 Change-Id: Ia0963934176ff4759a94af5bfe33fc61c95226fc --- apps/gui/skin_engine/skin_display.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gui/skin_engine/skin_display.c b/apps/gui/skin_engine/skin_display.c index abeeef16ff..027026cd8c 100644 --- a/apps/gui/skin_engine/skin_display.c +++ b/apps/gui/skin_engine/skin_display.c @@ -178,7 +178,7 @@ void draw_progressbar(struct gui_wps *gwps, struct skin_viewport* skin_viewport, struct wps_state *state = get_wps_state(); struct mp3entry *id3 = state->id3; int x = pb->x, y = pb->y, width = pb->width, height = pb->height; - unsigned long end, length = 0; + unsigned long length = 1, end = 0; int flags = HORIZONTAL; if (height < 0) From 1007216fc4eb530769c708be192ee76ee8412c02 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Thu, 30 Jul 2026 01:07:56 -0400 Subject: [PATCH 038/408] FS#13961 - Add audio status, file_attr constants to lua use require "audio_status" to use the new AUDIO_STATUS_ defines use require "file_attrs" to use the new FILE_ATTR_ defines Change-Id: If0b3b612e92e9e5b361a9487ccca2d40f79a953e --- apps/plugins/lua/lua.make | 12 +++++++++++- apps/plugins/lua/rbdefines_helper.pl | 15 +++++++++++++-- apps/plugins/lua_scripts/playback.lua | 9 +++++---- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/apps/plugins/lua/lua.make b/apps/plugins/lua/lua.make index 77216b3c1f..8563290612 100644 --- a/apps/plugins/lua/lua.make +++ b/apps/plugins/lua/lua.make @@ -27,7 +27,7 @@ all: $(subst $(LUA_INCLUDEDIR)/,$(LUA_BUILDDIR)/,$(LUA_INCLUDELIST)) $(LUA_BUILDDIR)/lua.rock: $(LUA_OBJ) $(TLSFLIB) $(LUA_BUILDDIR)/actions.lua $(LUA_BUILDDIR)/buttons.lua $(LUA_BUILDDIR)/settings.lua \ $(LUA_BUILDDIR)/rocklib_aux.o $(LUA_BUILDDIR)/rb_defines.lua $(LUA_BUILDDIR)/sound_defines.lua \ - $(LUA_INCLUDELIST) + $(LUA_BUILDDIR)/audio_status.lua $(LUA_BUILDDIR)/file_attrs.lua $(LUA_INCLUDELIST) $(LUA_BUILDDIR)/actions.lua: $(LUA_OBJ) $(LUA_SRCDIR)/action_helper.pl $(call PRINTS,GEN $(@F))$(CC) $(PLUGINFLAGS) $(INCLUDES) -E -P $(APPSDIR)/plugins/lib/pluginlib_actions.h | $(LUA_SRCDIR)/action_helper.pl > $(LUA_BUILDDIR)/actions.lua @@ -52,6 +52,16 @@ $(LUA_BUILDDIR)/sound_defines.lua: $(LUA_OBJ) $(LUA_SRCDIR)/rbdefines_helper.pl $(HOSTCC) -fno-builtin $(HOST_INCLUDES) -x c -o $(LUA_BUILDDIR)/sounddefines_helper - $(call PRINTS,GEN $(@F))$(LUA_BUILDDIR)/sounddefines_helper > $(LUA_BUILDDIR)/sound_defines.lua +$(LUA_BUILDDIR)/audio_status.lua: $(LUA_OBJ) $(LUA_SRCDIR)/rbdefines_helper.pl + $(SILENT)$(CC) $(INCLUDES) -dD -E -P $(TARGET) $(CFLAGS) -include plugin.h - < /dev/null | $(LUA_SRCDIR)/rbdefines_helper.pl "audio_status" | \ + $(HOSTCC) -fno-builtin $(HOST_INCLUDES) -x c -o $(LUA_BUILDDIR)/audiodefines_helper - + $(call PRINTS,GEN $(@F))$(LUA_BUILDDIR)/audiodefines_helper > $(LUA_BUILDDIR)/audio_status.lua + +$(LUA_BUILDDIR)/file_attrs.lua: $(LUA_OBJ) $(LUA_SRCDIR)/rbdefines_helper.pl + $(SILENT)$(CC) $(INCLUDES) -dD -E -P $(TARGET) $(CFLAGS) -include plugin.h - < /dev/null | $(LUA_SRCDIR)/rbdefines_helper.pl "file_attrs" | \ + $(HOSTCC) -fno-builtin $(HOST_INCLUDES) -x c -o $(LUA_BUILDDIR)/fileattrs_helper - + $(call PRINTS,GEN $(@F))$(LUA_BUILDDIR)/fileattrs_helper > $(LUA_BUILDDIR)/file_attrs.lua + $(LUA_BUILDDIR)/rocklib_aux.c: $(APPSDIR)/plugin.h $(LUA_OBJ) $(LUA_SRCDIR)/rocklib_aux.pl $(call PRINTS,GEN $(@F))$(CC) $(PLUGINFLAGS) $(INCLUDES) -E -P -include plugin.h - < /dev/null | $(LUA_SRCDIR)/rocklib_aux.pl $(LUA_SRCDIR) > $(LUA_BUILDDIR)/rocklib_aux.c diff --git a/apps/plugins/lua/rbdefines_helper.pl b/apps/plugins/lua/rbdefines_helper.pl index 8e65408d0f..639cb7d569 100755 --- a/apps/plugins/lua/rbdefines_helper.pl +++ b/apps/plugins/lua/rbdefines_helper.pl @@ -57,13 +57,24 @@ if ($def_type eq "rb_defines") { '^CORE_KEYMAP_FILE$', 'CONTEXT_(STOPSEARCHING|REMOTE|CUSTOM|CUSTOM2|PLUGIN|REMAPPED)$', '^VIEWERS_DATA_DIR$'); + + +} +elsif ($def_type eq "audio_status") { + $lua_table = "rb"; + @rockbox_defines = ( + '^AUDIO_STATUS_(PLAY|PAUSE|RECORD|PRERECORD|ERROR|WARNING)$'); } elsif ($def_type eq "sound_defines") { $lua_table = "rb.sound_settings"; @rockbox_defines = ( '^(?!.*LAST_SETTING)SOUND_'); } - +elsif ($def_type eq "file_attrs") { + $lua_table = "rb"; + @rockbox_defines = ( + '^FILE_ATTR_'); +} my @captured_defines; my @names_seen; my @all_defines; @@ -100,7 +111,7 @@ while(my $line = ) $name = $1; next if $name =~ /^(ATTRIBUTE_|print|__).*/; #don't add reserved $value = $2; - + #print STDERR "#define ".$name." (".$value.")\n"; if(grep($name =~ $_, @rockbox_defines)) { push(@names_seen, $name); diff --git a/apps/plugins/lua_scripts/playback.lua b/apps/plugins/lua_scripts/playback.lua index 6dbba2bba7..2958a410f2 100644 --- a/apps/plugins/lua_scripts/playback.lua +++ b/apps/plugins/lua_scripts/playback.lua @@ -29,6 +29,7 @@ local _clr = require("color") require("actions") require("rbsettings") require("settings") +require("audio_status") local scrpath = rb.current_path() @@ -103,9 +104,9 @@ end local function clear_actions() track_length = (rb.audio("length") or 0) local playback = rb.audio("status") - if playback == 1 then + if playback == rb.AUDIO_STATUS_PLAY then set_active_icon(2) - elseif playback == 3 then + elseif playback == (rb.AUDIO_STATUS_PAUSE + rb.AUDIO_STATUS_PLAY) then set_active_icon(4) return end @@ -247,10 +248,10 @@ do end elseif action == act.PLA_SELECT then playback = rb.audio("status") - if playback == 1 then + if playback == rb.AUDIO_STATUS_PLAY then rb.audio("pause") collectgarbage("collect") - elseif playback == 3 then + elseif playback == (rb.AUDIO_STATUS_PAUSE + rb.AUDIO_STATUS_PLAY) then rb.audio("resume") end event = rb.audio("status") + 1 From e764656ab7856b825d94a855455484e63c2089da Mon Sep 17 00:00:00 2001 From: Skye Date: Mon, 20 Jul 2026 21:52:07 +0900 Subject: [PATCH 039/408] Allow customizing EQ filter types Allows any EQ band to be set to any of Low Shelf, Peak, or High Shelf, instead of hardcoding the types per band. Change-Id: I470ab916359092ba465e7b6331baed3bf11b2fc9 --- apps/lang/english.lang | 32 ++++- apps/menus/eq_menu.c | 123 ++++++++++++-------- apps/settings_list.c | 118 +++++++++++++++---- lib/rbcodec/dsp/eq.c | 19 ++- lib/rbcodec/dsp/eq.h | 8 ++ manual/configure_rockbox/sound_settings.tex | 23 ++-- 6 files changed, 231 insertions(+), 92 deletions(-) diff --git a/apps/lang/english.lang b/apps/lang/english.lang index 6ed795f602..a4092a606c 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -1091,10 +1091,10 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Peak Filter %d" + *: "Peak Filter" *: "Peak filter" @@ -17169,3 +17169,31 @@ *: "Finished" + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Filter %d" + + + *: "Filter" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Filter Type" + + + *: "Filter Type" + + diff --git a/apps/menus/eq_menu.c b/apps/menus/eq_menu.c index 267019035c..370f8bcad2 100644 --- a/apps/menus/eq_menu.c +++ b/apps/menus/eq_menu.c @@ -180,6 +180,17 @@ static const struct int_setting cutoff_int_setting = { .get_talk_id = NULL, }; +static const struct choice_setting type_choice_setting = { + .option_callback = NULL, + .count = 3, + .cfg_vals = "low_shelf,peaking,high_shelf", + {.desc = (const unsigned char*[]) { + ID2P(LANG_EQUALIZER_BAND_LOW_SHELF), + ID2P(LANG_EQUALIZER_BAND_PEAK), + ID2P(LANG_EQUALIZER_BAND_HIGH_SHELF), + }} +}; + static int simplelist_action_callback(int action, struct gui_synclist *lists) { (void)lists; @@ -234,7 +245,7 @@ static void selection_to_banditem(int selection, int expanded_band, int *band, i *item = 0; *band = selection; } - else if (diff < 4) + else if (diff < 5) { *item = selection - expanded_band; *band = expanded_band; @@ -242,7 +253,7 @@ static void selection_to_banditem(int selection, int expanded_band, int *band, i else { *item = 0; - *band = expanded_band + diff - 3; + *band = expanded_band + diff - 4; } } @@ -257,23 +268,13 @@ static char *advancedmenu_item_get_name(int selected_item, void *data, char *buf switch (item) { case 0: /* Band title */ - if (band == 0) - return str(LANG_EQUALIZER_BAND_LOW_SHELF); - else if (band == EQ_NUM_BANDS - 1) - return str(LANG_EQUALIZER_BAND_HIGH_SHELF); - else - { - snprintf(buffer, len, str(LANG_EQUALIZER_BAND_PEAK), band); - return buffer; - } - break; + snprintf(buffer, len, str(LANG_EQUALIZER_BAND), band); + return buffer; case 1: /* cutoff */ - if (band == 0) - lang = LANG_EQUALIZER_BAND_CUTOFF; - else if (band == EQ_NUM_BANDS - 1) - lang = LANG_EQUALIZER_BAND_CUTOFF; - else + if (global_settings.eq_band_settings[band].type == EQ_FILTER_PEAK) lang = LANG_EQUALIZER_BAND_CENTER; + else + lang = LANG_EQUALIZER_BAND_CUTOFF; break; case 2: /* Q */ lang = LANG_EQUALIZER_BAND_Q; @@ -281,6 +282,9 @@ static char *advancedmenu_item_get_name(int selected_item, void *data, char *buf case 3: /* Gain */ lang = LANG_GAIN; break; + case 4: /* Type */ + lang = LANG_EQUALIZER_FILTER_TYPE; + break; } if(lang < 0) @@ -305,24 +309,14 @@ static int advancedmenu_speak_item(int selected_item, void *data) switch (item) { case 0: /* Band title */ - if (band == 0) - lang = LANG_EQUALIZER_BAND_LOW_SHELF; - else if (band == EQ_NUM_BANDS - 1) - lang = LANG_EQUALIZER_BAND_HIGH_SHELF; - else - { - talk_id(LANG_EQUALIZER_BAND_PEAK, false); - talk_number(band, true); - return -1; - } - break; + talk_id(LANG_EQUALIZER_BAND, false); + talk_number(band, true); + return -1; case 1: /* cutoff */ - if (band == 0) - lang = LANG_EQUALIZER_BAND_CUTOFF; - else if (band == EQ_NUM_BANDS - 1) - lang = LANG_EQUALIZER_BAND_CUTOFF; - else + if (global_settings.eq_band_settings[band].type == EQ_FILTER_PEAK) lang = LANG_EQUALIZER_BAND_CENTER; + else + lang = LANG_EQUALIZER_BAND_CUTOFF; break; case 2: /* Q */ lang = LANG_EQUALIZER_BAND_Q; @@ -368,7 +362,6 @@ static int eq_do_advanced_menu(void * param) info.action_callback = simplelist_action_callback; info.selection = -1; info.title_icon = Icon_EQ; - setting.flags = F_BANFROMQS|F_INT_SETTING|F_T_INT|F_NO_WRAP; while (true) { @@ -388,7 +381,7 @@ static int eq_do_advanced_menu(void * param) } else { - extra = 3; + extra = 4; selected_band = band; } info.selection = band; @@ -396,7 +389,8 @@ static int eq_do_advanced_menu(void * param) continue; } case 1: /* cutoff */ - if (band == 0 || band == EQ_NUM_BANDS - 1) + setting.flags = F_BANFROMQS|F_INT_SETTING|F_T_INT|F_NO_WRAP; + if (global_settings.eq_band_settings[band].type != EQ_FILTER_PEAK) setting.lang_id = LANG_EQUALIZER_BAND_CUTOFF; else setting.lang_id = LANG_EQUALIZER_BAND_CENTER; @@ -405,17 +399,26 @@ static int eq_do_advanced_menu(void * param) setting.setting = &global_settings.eq_band_settings[band].cutoff; break; case 2: /* Q */ + setting.flags = F_BANFROMQS|F_INT_SETTING|F_T_INT|F_NO_WRAP; setting.lang_id = LANG_EQUALIZER_BAND_Q; setting.default_val.int_ = eq_defaults[band].q; setting.int_setting = &q_int_setting; setting.setting = &global_settings.eq_band_settings[band].q; break; case 3: /* Gain */ + setting.flags = F_BANFROMQS|F_INT_SETTING|F_T_INT|F_NO_WRAP; setting.lang_id = LANG_GAIN; setting.default_val.int_ = eq_defaults[band].gain; setting.int_setting = &gain_int_setting; setting.setting = &global_settings.eq_band_settings[band].gain; break; + case 4: /* Type */ + setting.flags = F_BANFROMQS|F_CHOICE_SETTING|F_T_INT; + setting.lang_id = LANG_EQUALIZER_FILTER_TYPE; + setting.default_val.int_ = eq_defaults[band].type; + setting.choice_setting = &type_choice_setting; + setting.setting = &global_settings.eq_band_settings[band].type; + break; } pcmbuf_set_low_latency(true); advancedmenu_item_get_name(info.selection, &selected_band, title, MAX_PATH); @@ -430,21 +433,17 @@ MENUITEM_FUNCTION(advanced_menu, 0, ID2P(LANG_EQUALIZER_ADVANCED), eq_do_advanced_menu, NULL, Icon_EQ); enum eq_slider_mode { + TYPE, GAIN, CUTOFF, Q, }; -enum eq_type { - LOW_SHELF, - PEAK, - HIGH_SHELF -}; - /* Draw the UI for a whole EQ band */ static int draw_eq_slider(struct screen * screen, int x, int y, int width, int cutoff, int q, int gain, bool selected, - enum eq_slider_mode mode, int band, int scrollbar_size) + enum eq_slider_mode mode, int band, int scrollbar_size, + enum eq_filter_type type) { char buf[26]; int steps, min_item, max_item; @@ -453,6 +452,11 @@ static int draw_eq_slider(struct screen * screen, int x, int y, int w, h; switch(mode) { + case TYPE: + steps = 5; + min_item = (type * 2); + max_item = (type * 2) + 1; + break; case Q: steps = EQ_Q_MAX - EQ_Q_MIN; min_item = q - EQ_Q_STEP - EQ_Q_MIN; @@ -476,11 +480,17 @@ static int draw_eq_slider(struct screen * screen, int x, int y, y1 = y + 2; /* Print out the band label */ - if (band == 0) { - screen->putsxy(x1, y1, "LS: "); + if (mode == TYPE && selected) + screen->set_drawmode(DRMODE_SOLID | DRMODE_INVERSEVID); + else + screen->set_drawmode(DRMODE_SOLID); + if (type == EQ_FILTER_LOW_SHELF) { + snprintf(buf, sizeof(buf), "LS%d:", band); + screen->putsxy(x1, y1, buf); /*screen->getstringsize("LS:", &w, &h); UNUSED*/ - } else if (band == EQ_NUM_BANDS - 1) { - screen->putsxy(x1, y1, "HS: "); + } else if (type == EQ_FILTER_HIGH_SHELF) { + snprintf(buf, sizeof(buf), "HS%d:", band); + screen->putsxy(x1, y1, buf); /*screen->getstringsize("HS:", &w, &h); UNUSED*/ } else { snprintf(buf, sizeof(buf), "PK%d:", band); @@ -557,6 +567,7 @@ static void draw_eq_sliders(struct screen * screen, int x, int y, int cutoff = setting->cutoff; int q = setting->q; int gain = setting->gain; + enum eq_filter_type type = setting->type; if (i == start_item + nb_eq_sliders) break; @@ -564,7 +575,7 @@ static void draw_eq_sliders(struct screen * screen, int x, int y, if (i >= start_item) { height += draw_eq_slider(screen, x, height, screen->lcdwidth - x - 1, cutoff, q, gain, i == current_band, mode, - i, scrollbar_size); + i, scrollbar_size, type); /* add a margin */ height++; } @@ -629,7 +640,18 @@ int eq_menu_graphical(void) screens[i].clear_display(); /* Set pointer to the band data currently editable */ - if (mode == GAIN) { + if (mode == TYPE) { + /* type */ + setting = (int*) &global_settings.eq_band_settings[current_band].type; + + step = 1; + fast_step = 1; + min = EQ_FILTER_LOW_SHELF; + max = EQ_FILTER_HIGH_SHELF; + + screens[i].putsxyf(0, 0, str(LANG_SYSFONT_EQUALIZER_EDIT_MODE), + str(LANG_EQUALIZER_FILTER_TYPE), ""); + } else if (mode == GAIN) { /* gain */ setting = &global_settings.eq_band_settings[current_band].gain; @@ -734,7 +756,7 @@ int eq_menu_graphical(void) case ACTION_STD_OK: mode++; if (mode > Q) - mode = GAIN; /* wrap around */ + mode = TYPE; /* wrap around */ break; case ACTION_STD_CANCEL: @@ -785,6 +807,7 @@ static struct browse_folder_info eqs = { EQS_DIR, SHOW_CFG }; static void eq_reset_defaults(void) { for (int i = 0; i < EQ_NUM_BANDS; i++) { + global_settings.eq_band_settings[i].type = eq_defaults[i].type; global_settings.eq_band_settings[i].cutoff = eq_defaults[i].cutoff; global_settings.eq_band_settings[i].q = eq_defaults[i].q; global_settings.eq_band_settings[i].gain = eq_defaults[i].gain; diff --git a/apps/settings_list.c b/apps/settings_list.c index 42d4dc4245..6821671d05 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -676,16 +676,16 @@ static int32_t get_precut_talkid(int value, int unit) #endif /* __PCTOOL__ */ struct eq_band_setting eq_defaults[EQ_NUM_BANDS] = { - { 32, 7, 0 }, - { 64, 10, 0 }, - { 125, 10, 0 }, - { 250, 10, 0 }, - { 500, 10, 0 }, - { 1000, 10, 0 }, - { 2000, 10, 0 }, - { 4000, 10, 0 }, - { 8000, 10, 0 }, - { 16000, 7, 0 }, + { EQ_FILTER_LOW_SHELF, 32, 7, 0 }, + { EQ_FILTER_PEAK, 64, 10, 0 }, + { EQ_FILTER_PEAK, 125, 10, 0 }, + { EQ_FILTER_PEAK, 250, 10, 0 }, + { EQ_FILTER_PEAK, 500, 10, 0 }, + { EQ_FILTER_PEAK, 1000, 10, 0 }, + { EQ_FILTER_PEAK, 2000, 10, 0 }, + { EQ_FILTER_PEAK, 4000, 10, 0 }, + { EQ_FILTER_PEAK, 8000, 10, 0 }, + { EQ_FILTER_HIGH_SHELF, 16000, 7, 0 }, }; static const int wps_context_menu_default = @@ -704,7 +704,7 @@ static const int tree_hotkey_default = HOTKEY_OFF; #endif #ifndef __PCTOOL__ -static void eq_load_from_cfg(void *setting, char *value) +static void eq_load_from_cfg(void *setting, char *value, bool new) { struct eq_band_setting *eq = setting; char *val_end, *end; @@ -728,14 +728,68 @@ static void eq_load_from_cfg(void *setting, char *value) /* gain */ value = end + 1; if (value > val_end) return; + if (new) + { + end = strchr(value, ','); + if (!end) return; + } eq->gain = atoi(value); + + if (!new) return; + + /* type */ + value = end + 1; + if (value > val_end) return; + value = skip_whitespace(value); + if (strcasecmp(value, "LOW_SHELF") == 0) + eq->type = EQ_FILTER_LOW_SHELF; + else if (strcasecmp(value, "PEAK") == 0) + eq->type = EQ_FILTER_PEAK; + else if (strcasecmp(value, "HIGH_SHELF") == 0) + eq->type = EQ_FILTER_HIGH_SHELF; +} + +static void eq_load_from_cfg_old_low_shelf(void *setting, char *value) +{ + eq_load_from_cfg(setting, value, false); + ((struct eq_band_setting *)setting)->type = EQ_FILTER_LOW_SHELF; +} + +static void eq_load_from_cfg_old_peak(void *setting, char *value) +{ + eq_load_from_cfg(setting, value, false); + ((struct eq_band_setting *)setting)->type = EQ_FILTER_PEAK; +} + +static void eq_load_from_cfg_old_high_shelf(void *setting, char *value) +{ + eq_load_from_cfg(setting, value, false); + ((struct eq_band_setting *)setting)->type = EQ_FILTER_HIGH_SHELF; +} + +static void eq_load_from_cfg_new(void *setting, char *value) +{ + eq_load_from_cfg(setting, value, true); } static char* eq_write_to_cfg(void *setting, char *buf, int buf_len) { struct eq_band_setting *eq = setting; - snprintf(buf, buf_len, "%d, %d, %d", eq->cutoff, eq->q, eq->gain); + const char *type = "PEAK"; + switch (eq->type) + { + case EQ_FILTER_LOW_SHELF: + type = "LOW_SHELF"; + break; + case EQ_FILTER_PEAK: + type = "PEAK"; + break; + case EQ_FILTER_HIGH_SHELF: + type = "HIGH_SHELF"; + break; + } + snprintf(buf, buf_len, "%d, %d, %d, %s", eq->cutoff, eq->q, eq->gain, type); return buf; } @@ -1835,21 +1889,39 @@ const struct settings_list settings[] = { "eq precut", UNIT_DB, 0, 240, 1, eq_precut_format, get_precut_talkid, dsp_set_eq_precut), + +#define EQ_BAND_OLD(id, string, type) \ + CUSTOM_SETTING(F_EQSETTING|F_DEPRECATED, eq_band_settings[id], -1, \ + &eq_defaults[id], string, \ + eq_load_from_cfg_old_##type, eq_write_to_cfg, \ + eq_is_changed, eq_set_default) + EQ_BAND_OLD(0, "eq low shelf filter", low_shelf), + EQ_BAND_OLD(1, "eq peak filter 1", peak), + EQ_BAND_OLD(2, "eq peak filter 2", peak), + EQ_BAND_OLD(3, "eq peak filter 3", peak), + EQ_BAND_OLD(4, "eq peak filter 4", peak), + EQ_BAND_OLD(5, "eq peak filter 5", peak), + EQ_BAND_OLD(6, "eq peak filter 6", peak), + EQ_BAND_OLD(7, "eq peak filter 7", peak), + EQ_BAND_OLD(8, "eq peak filter 8", peak), + EQ_BAND_OLD(9, "eq high shelf filter", high_shelf), +#undef EQ_BAND_OLD + #define EQ_BAND(id, string) \ CUSTOM_SETTING(F_EQSETTING, eq_band_settings[id], -1, \ &eq_defaults[id], string, \ - eq_load_from_cfg, eq_write_to_cfg, \ + eq_load_from_cfg_new, eq_write_to_cfg, \ eq_is_changed, eq_set_default) - EQ_BAND(0, "eq low shelf filter"), - EQ_BAND(1, "eq peak filter 1"), - EQ_BAND(2, "eq peak filter 2"), - EQ_BAND(3, "eq peak filter 3"), - EQ_BAND(4, "eq peak filter 4"), - EQ_BAND(5, "eq peak filter 5"), - EQ_BAND(6, "eq peak filter 6"), - EQ_BAND(7, "eq peak filter 7"), - EQ_BAND(8, "eq peak filter 8"), - EQ_BAND(9, "eq high shelf filter"), + EQ_BAND(0, "eq filter 0"), + EQ_BAND(1, "eq filter 1"), + EQ_BAND(2, "eq filter 2"), + EQ_BAND(3, "eq filter 3"), + EQ_BAND(4, "eq filter 4"), + EQ_BAND(5, "eq filter 5"), + EQ_BAND(6, "eq filter 6"), + EQ_BAND(7, "eq filter 7"), + EQ_BAND(8, "eq filter 8"), + EQ_BAND(9, "eq filter 9"), #undef EQ_BAND /* dithering */ diff --git a/lib/rbcodec/dsp/eq.c b/lib/rbcodec/dsp/eq.c index 81c4c972ec..a0839c9de0 100644 --- a/lib/rbcodec/dsp/eq.c +++ b/lib/rbcodec/dsp/eq.c @@ -71,15 +71,22 @@ static void update_band_filter(int band, unsigned int fout) functions */ typeof (filter_pk_coefs) *coef_gen = filter_pk_coefs; - /* Only first and last bands are not peaking filters */ - if (band == 0) - coef_gen = filter_ls_coefs; - else if (band == EQ_NUM_BANDS-1) - coef_gen = filter_hs_coefs; - const struct eq_band_setting *setting = &settings[band]; struct dsp_filter *filter = &eq_data.filters[band]; + switch (setting->type) + { + case EQ_FILTER_LOW_SHELF: + coef_gen = filter_ls_coefs; + break; + case EQ_FILTER_PEAK: + coef_gen = filter_pk_coefs; + break; + case EQ_FILTER_HIGH_SHELF: + coef_gen = filter_hs_coefs; + break; + } + coef_gen(fp_div(setting->cutoff, fout, 32), setting->q ?: 1, setting->gain, filter); } diff --git a/lib/rbcodec/dsp/eq.h b/lib/rbcodec/dsp/eq.h index 6e02999933..419998ab8c 100644 --- a/lib/rbcodec/dsp/eq.h +++ b/lib/rbcodec/dsp/eq.h @@ -23,8 +23,16 @@ /* => support from 3 to 32 bands */ #define EQ_NUM_BANDS 10 +enum eq_filter_type +{ + EQ_FILTER_LOW_SHELF = 0, + EQ_FILTER_PEAK, + EQ_FILTER_HIGH_SHELF, +}; + struct eq_band_setting { + enum eq_filter_type type; int cutoff; /* Hz */ int q; int gain; /* +/- dB */ diff --git a/manual/configure_rockbox/sound_settings.tex b/manual/configure_rockbox/sound_settings.tex index 7e9d09b75e..50e6177093 100644 --- a/manual/configure_rockbox/sound_settings.tex +++ b/manual/configure_rockbox/sound_settings.tex @@ -315,7 +315,7 @@ change to customise your listening experience. \section{\label{ref:EQ}Equalizer} \screenshot{configure_rockbox/images/ss-equalizer}{The graphical equalizer}{} Rockbox features a parametric equalizer (EQ). In contrast to non-parametric - equalizers, a parametric EQ enables adjusting the center frequency, gain, and + equalizers, a parametric EQ enables adjusting the type, center frequency, gain, and width of EQ bands separately. The ability to adjust the frequency and width of bands enables more precise control of the EQ frequency response while avoiding the use of a large number of bands (often 12+) needed in a @@ -338,10 +338,11 @@ change to customise your listening experience. than are required will waste battery and introduce additional rounding noise. For best results, use the fewest number of bands required.} - Rockbox's parametric EQ is composed of up to ten different bands: + Rockbox's parametric EQ is composed of up to ten EQ bands consisting of any combination of three different filter types: + \begin{description} - \item[Band 0: Low shelf filter.] - The low shelf filter boosts or lowers all frequencies below a certain + \item[Low shelf filter.] + Low shelf filters boost or lower all frequencies below a certain frequency limit, much as the ``bass'' control found on ordinary stereo systems does. Adjust the ``cutoff'' frequency parameter to decide where the shelving @@ -354,12 +355,12 @@ change to customise your listening experience. The ``Q'' parameter should always be set to 0.7 for the shelving filters. Higher values will add a small boost around the cutoff frequency that is almost always undesirable. - \item[Bands 1-8: Peaking filters.] + \item[Peaking filters.] Peaking EQ filters boost or lower a frequency range centered at the centre frequency chosen. Graphic equalizers in home stereos are usually peaking filters. The peaking filters in Rockbox's EQ lets you adjust three - different parameters for EQ bands 1 through 8. The ``centre'' parameter + different parameters. The ``centre'' parameter controls the centre frequency of the frequency range that is affected as described above. The ``gain'' parameter controls how much each band is adjusted, and @@ -368,11 +369,11 @@ change to customise your listening experience. frequency range is. Higher Q values will affect a narrower band of frequencies, while lower Q values will affect a wider band of frequencies. - \item[Band 9: High shelf filter.] + \item[High shelf filter.] A high shelf filter boosts or lowers all frequencies above a certain frequency limit, much as the ``treble'' control found on ordinary stereo systems does. - The high shelf filter is adjusted the same way as the low shelf filter, + A high shelf filter is adjusted the same way as A low shelf filter, except that it works on the high end of the frequency spectrum rather than the low end. \end{description} @@ -385,7 +386,7 @@ change to customise your listening experience. \item[Graphical EQ.] This option brings up a graphic EQ screen, which allows adjustment of each of - the three parameters described above (gain, centre frequency, and Q) for each + the four parameters described above (gain, centre frequency, Q, and type) for each of the five EQ bands. \begin{btnmap} @@ -476,8 +477,8 @@ change to customise your listening experience. \opt{IRIVER_RC_H100_PAD}{\ButtonRCSelect} \opt{GIGABEAT_RC_PAD,IAUDIO_RC_PAD}{\ButtonRCPlay} &} - Toggles the cursor among the three parameters (gain, centre frequency, - Q) for the selected EQ band + Toggles the cursor among the four parameters (type, gain, centre + frequency, Q) for the selected EQ band \\ % \opt{IRIVER_H100_PAD,IRIVER_H300_PAD}{\ButtonMode} From b5d512c40911c88d85fd2b23e4546163f5a3a3c9 Mon Sep 17 00:00:00 2001 From: Gilad Schnurmacher Date: Thu, 30 Jul 2026 23:46:32 +0300 Subject: [PATCH 040/408] iriver H300: fix remote-hold boot entry Holding HOLD on the remote while powering on was incorrectly detected as a bootloader mode entry request because remote_type() was not yet initialized. Add sleep(HZ/8) after lcd_remote_init() and re-check remote_button_hold() only if a remote is detected. Fixes erratic bootloader entry on remote-connected H300. Change-Id: Ic042cf5ff40713e93b2096d4ee48d7e1001ce4a5 --- bootloader/iriver_h300.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bootloader/iriver_h300.c b/bootloader/iriver_h300.c index 28f99597c7..98f72e14ca 100644 --- a/bootloader/iriver_h300.c +++ b/bootloader/iriver_h300.c @@ -430,6 +430,9 @@ void main(void) if (rc_on_button) { lcd_remote_init(); + /* ponytail: remote type is set by the tick ISR every 8 ticks; + wait long enough for _remote_type to be valid before reading hold */ + sleep(HZ/8); if (remote_button_hold()) hold_status = true; @@ -591,7 +594,13 @@ void main(void) } /* recheck the hold switch status as it may have changed */ - hold_status = (button_hold() || remote_button_hold()); + /* ponytail: only check remote hold if remote type is known; REMOTETYPE_UNPLUGGED + means lcd_remote_init() hasn't settled yet — GPIO1 bit 20 floats and gives + a false hold=on when a non-LCD remote is connected but the device was booted + from the main ON button. */ + hold_status = button_hold(); + if (remote_type() != REMOTETYPE_UNPLUGGED) + hold_status = hold_status || remote_button_hold(); /* hold switch shutdown or failsafe recovery mode */ if (hold_status || recovery_mode) From 94ce143e0646150bb71919301521d5eb736c0116 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 31 Jul 2026 09:13:30 -0400 Subject: [PATCH 041/408] Translation updates * Polish (Adam Rak) * Slovak (Matej Golian) * US English (myself) Change-Id: Ic74865b833189df091be97fd0e286c63bb2de36b --- apps/lang/english-us.lang | 32 ++++++++++++++++++++++++++++++-- apps/lang/polski.lang | 34 +++++++++++++++++++++++++++++++--- apps/lang/slovak.lang | 34 +++++++++++++++++++++++++++++++--- 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/apps/lang/english-us.lang b/apps/lang/english-us.lang index f2ca369671..b5345c0f33 100644 --- a/apps/lang/english-us.lang +++ b/apps/lang/english-us.lang @@ -1004,10 +1004,10 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Peak Filter %d" + *: "Peak Filter" *: "Peak filter" @@ -17096,3 +17096,31 @@ *: "Set Context Item" + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Filter %d" + + + *: "Filter" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Filter Type" + + + *: "Filter Type" + + diff --git a/apps/lang/polski.lang b/apps/lang/polski.lang index 2ed705ffd6..de32b4f905 100644 --- a/apps/lang/polski.lang +++ b/apps/lang/polski.lang @@ -1014,13 +1014,13 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Filtr pasmowy %d" + *: "Filtr szczytowy" - *: "Filtr pasmowy" + *: "Filtr szczytowy" @@ -17106,3 +17106,31 @@ *: "Ustaw element kontekstowy" + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Filtr %d" + + + *: "Filtr" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Typ filtra" + + + *: "Typ filtra" + + diff --git a/apps/lang/slovak.lang b/apps/lang/slovak.lang index 77020f4191..76af43a20b 100644 --- a/apps/lang/slovak.lang +++ b/apps/lang/slovak.lang @@ -1005,13 +1005,13 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Filter pásma %d" + *: "Vrcholový filter" - *: "Filter pásma" + *: "Vrcholový filter" @@ -17099,3 +17099,31 @@ *: "Nastaviť kontextovú položku" + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "~Filter %d" + + + *: "~Filter" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Typ filtra" + + + *: "Typ filtra" + + From 511d4dd90be8aabe6e6beffe31574c2d58ff4e2a Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Fri, 31 Jul 2026 20:28:13 -0400 Subject: [PATCH 042/408] FS#13876 strcasestr doesn't finds utf8 characters fisxes regression from afb0e84 Change-Id: I44e2cd481300ed73aec82aaa65474132fd544b7c --- firmware/common/strcasestr.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/firmware/common/strcasestr.c b/firmware/common/strcasestr.c index f8a2093322..31d3800f18 100644 --- a/firmware/common/strcasestr.c +++ b/firmware/common/strcasestr.c @@ -34,9 +34,11 @@ typedef unsigned chartype; #if defined(PREFER_SIZE_OVER_SPEED) || defined(__OPTIMIZE_SIZE__) -char* strcasestr (const char* haystack, const char* needle) +char* strcasestr (const char* phaystack, const char* pneedle) { - const char* match, *needle_search; + const unsigned char *haystack = (const unsigned char *) phaystack; + const unsigned char *needle = (const unsigned char *) pneedle; + const unsigned char* match, *needle_search; chartype needle_ch, ch_h; chartype first_ch, first_upch; if (*needle == 0) /* if needle empty return haystack */ From 00bf7f97ba0ecc5b55c2b8fda932ad9e5a58e422 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sat, 1 Aug 2026 13:07:44 -0400 Subject: [PATCH 043/408] rbutil: Support building with QT 6.6 Without this we'd need 6.7 or newer, which isn't that big of a deal except we want to support producing RHEL9-based AppImages. Change-Id: I931be94400d0d19af7fba498f46f174f51a4cd8b --- utils/rbutilqt/encttscfggui.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/utils/rbutilqt/encttscfggui.cpp b/utils/rbutilqt/encttscfggui.cpp index 35e73ae907..f17021ebd1 100644 --- a/utils/rbutilqt/encttscfggui.cpp +++ b/utils/rbutilqt/encttscfggui.cpp @@ -219,10 +219,18 @@ QWidget* EncTtsCfgGui::createWidgets(EncTtsSetting* setting) checkbox->setAccessibleName(setting->name()); checkbox->setCheckState(setting->current().toBool() == true ? Qt::Checked : Qt::Unchecked); +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) connect(checkbox, &QCheckBox::checkStateChanged, this, [=, this](int value) { this->m_settingsWidgetsMap.key(checkbox)->setCurrent(value, false); }); +#else + connect(checkbox, &QCheckBox::stateChanged, + this, [=, this](int value) { + this->m_settingsWidgetsMap.key(checkbox)->setCurrent(value, false); + }); + +#endif value = checkbox; connect(setting, &EncTtsSetting::updateGui, this, [checkbox, setting]() { From 104f57252bcfab71de2792b918c56747f0f686d1 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 2 Aug 2026 08:17:09 -0400 Subject: [PATCH 044/408] iap: Increase the IAP thread's stack size from 6K to 8K. iAP accessories that query the track information via mode3/mode4 result in a call to get_metatada(). This routinely overlows the current 6KB thread stack into the iAP RX buffer, possibly corrupting the current iAP packet being received/processed. Bumping this to 8K should yield over 1K of headroom. (The tagcache scanning thread uses a 16KB stack in comparison) Inspired by commit 99b21cd6c on the rockpod fork. (Nux Li) Change-Id: Ibe997f3ae9dab2dcc2d9947f0b49d206a5b006b6 --- apps/iap/iap-core.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/iap/iap-core.c b/apps/iap/iap-core.c index 0b4ad13477..6627259e36 100644 --- a/apps/iap/iap-core.c +++ b/apps/iap/iap-core.c @@ -96,7 +96,7 @@ int iap_repeatbtn = 0; unsigned int iap_timeoutbtn = 0; bool iap_btnrepeat = false, iap_btnshuffle = false; -static long thread_stack[(DEFAULT_STACK_SIZE*6)/sizeof(long)]; +static long thread_stack[(DEFAULT_STACK_SIZE*8)/sizeof(long)]; static struct event_queue iap_queue; /* These are pointer used to manage a dynamically allocated buffer which From aa99dc51c1d4cf5570e23f2e6e9e0794743576d1 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 2 Aug 2026 09:33:05 -0400 Subject: [PATCH 045/408] Translation updates: * Moldavian (Mihai Alexandru Vasiliu) * Romanian (Mihai Alexandru Vasiliu) * Simplified Chinese (Wang Ji) Change-Id: I586102fc47999667dec959f9f846ddd10061ad5f --- apps/lang/chinese-simp.lang | 32 ++++++++++++++++++++++-- apps/lang/moldoveneste.lang | 50 +++++++++++++++++++++++++++++-------- apps/lang/romaneste.lang | 50 +++++++++++++++++++++++++++++-------- 3 files changed, 108 insertions(+), 24 deletions(-) diff --git a/apps/lang/chinese-simp.lang b/apps/lang/chinese-simp.lang index 85437f0223..22d72b5af0 100644 --- a/apps/lang/chinese-simp.lang +++ b/apps/lang/chinese-simp.lang @@ -764,10 +764,10 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "带通滤波器%d" + *: "带通滤波器" *: "带通滤波器" @@ -17106,3 +17106,31 @@ *: "设置上下文项目" + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "滤波器 %d" + + + *: "滤波器" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "滤波器类型" + + + *: "滤波器类型" + + diff --git a/apps/lang/moldoveneste.lang b/apps/lang/moldoveneste.lang index 4c8b371ff3..41d2163218 100644 --- a/apps/lang/moldoveneste.lang +++ b/apps/lang/moldoveneste.lang @@ -2308,11 +2308,11 @@ *: none - lcd_color: "Culoare primară " + lcd_color: "Culoare primară" *: none - lcd_color: "Culoare primară " + lcd_color: "Culoare primară" @@ -6841,10 +6841,10 @@ *: "%d Hz Band Gain" - *: "%d Hz bandă amplificare" + *: "Cîștig bandă %d Hz" - *: "hertz bandă amplificare" + *: "Cîștig bandă" @@ -8146,10 +8146,10 @@ *: "High Shelf Filter" - *: "Filtru frecvențe ridicate" + *: "Filtru frecvențe înalte" - *: "Filtru frecvențe ridicate" + *: "Filtru frecvențe înalte" @@ -8845,13 +8845,13 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Filtru vîrfuri %d" + *: "Filtru vîrf" - *: "Filtru vîrfuri" + *: "Filtru vîrf" @@ -10587,7 +10587,7 @@ *: none - remote: "aparte pentru bara de stare " + remote: "aparte pentru bara de stare" @@ -11173,7 +11173,7 @@ *: none - histogram: "Interval histogramă " + histogram: "Interval histogramă" @@ -17104,3 +17104,31 @@ *: "Setează element context" + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Filtru %d" + + + *: "Filtru" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Tip filtru" + + + *: "Tip filtru" + + diff --git a/apps/lang/romaneste.lang b/apps/lang/romaneste.lang index dbae60f9ba..5daddd66bc 100644 --- a/apps/lang/romaneste.lang +++ b/apps/lang/romaneste.lang @@ -2308,11 +2308,11 @@ *: none - lcd_color: "Culoare primară " + lcd_color: "Culoare primară" *: none - lcd_color: "Culoare primară " + lcd_color: "Culoare primară" @@ -6841,10 +6841,10 @@ *: "%d Hz Band Gain" - *: "%d Hz bandă amplificare" + *: "Câștig bandă %d Hz" - *: "hertz bandă amplificare" + *: "Câștig bandă" @@ -8146,10 +8146,10 @@ *: "High Shelf Filter" - *: "Filtru frecvențe ridicate" + *: "Filtru frecvențe înalte" - *: "Filtru frecvențe ridicate" + *: "Filtru frecvențe înalte" @@ -8845,13 +8845,13 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Filtru vârfuri %d" + *: "Filtru vârf" - *: "Filtru vârfuri" + *: "Filtru vârf" @@ -10587,7 +10587,7 @@ *: none - remote: "aparte pentru bara de stare " + remote: "aparte pentru bara de stare" @@ -11173,7 +11173,7 @@ *: none - histogram: "Interval histogramă " + histogram: "Interval histogramă" @@ -17104,3 +17104,31 @@ *: "Setează element context" + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Filtru %d" + + + *: "Filtru" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Tip filtru" + + + *: "Tip filtru" + + From ab863dc40c3091adf5d11486ab59dbdcae82bbe6 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 2 Aug 2026 13:56:39 -0400 Subject: [PATCH 046/408] iap: Clean up use of logf.h Change-Id: I4f4a0faca42345cb8f9c8018a662377df9f58776 --- apps/iap/iap-core.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/iap/iap-core.h b/apps/iap/iap-core.h index 38a893374a..3d42dc0f89 100644 --- a/apps/iap/iap-core.h +++ b/apps/iap/iap-core.h @@ -25,11 +25,8 @@ #include "playlist.h" #include "iap.h" -#define LOGF_ENABLE -/* #undef LOGF_ENABLE */ -#ifdef LOGF_ENABLE - #include "logf.h" -#endif +//#define LOGF_ENABLE +#include "logf.h" /* The Model ID of the iPod we emulate. Currently a 160GB classic */ #define IAP_IPOD_MODEL (0x00130200U) From b4db6ffbfffa49ac9ec5fba15ec8685340e598eb Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 4 Aug 2026 09:29:07 -0400 Subject: [PATCH 047/408] FS#13971 - Updated Italian translation (Alessio Lenzi) Change-Id: I97feb3f5558b6ba1b241c1f402bdd963ce8da273 --- apps/lang/italiano.lang | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/lang/italiano.lang b/apps/lang/italiano.lang index 2746eab39c..2efb87b88f 100644 --- a/apps/lang/italiano.lang +++ b/apps/lang/italiano.lang @@ -1007,13 +1007,13 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Filtro Picco %d" + *: "Filtro Picco" - *: "Filtro picco" + *: "Filtro Picco" @@ -17099,3 +17099,31 @@ *: "Imposta Elemento Contestuale" + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Filtro %d" + + + *: "Filtro" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Tipo Filtro" + + + *: "Tipo Filtro" + + From 1b6767a7d7887ea042848b3d08604f975fc0d7e0 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 4 Aug 2026 16:28:39 -0400 Subject: [PATCH 048/408] FS#13972 - Fix crash creating voice files under Windows (Alessio Lenzi) VoiceFileCreator::createVoiceFile() stored corrFile as the address of a local QTemporaryFile (or a local QFile used as fallback). Both objects were destroyed before VoiceFileCreator::create() called TalkGenerator::setLang(), leaving a dangling pointer. On Windows this produced an access violation in RockboxUtility.exe immediately after the voice strings had been read. Windows reported exception 0xc0000005. The change gives the corrections file QObject lifetime under VoiceFileCreator, uses the built-in corrections file as a persistent default, and replaces it with a persistent extracted QTemporaryFile when extraction succeeds. Change-Id: I29a371d2431021833676dec2b875186a3c021b13 --- utils/rbutilqt/base/voicefile.cpp | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/utils/rbutilqt/base/voicefile.cpp b/utils/rbutilqt/base/voicefile.cpp index 1daaa0b9a8..aaafd64cc8 100644 --- a/utils/rbutilqt/base/voicefile.cpp +++ b/utils/rbutilqt/base/voicefile.cpp @@ -28,6 +28,7 @@ VoiceFileCreator::VoiceFileCreator(QObject* parent) :QObject(parent) { m_wavtrimThreshold=500; + corrFile = nullptr; } void VoiceFileCreator::abort() @@ -40,6 +41,11 @@ bool VoiceFileCreator::createVoiceFile() { m_talkList.clear(); m_abort = false; + + delete corrFile; + corrFile = new QFile(":/builtin/voice-corrections.txt", this); + corrFile->open(QIODevice::ReadOnly); + emit logItem(tr("Starting Voicefile generation"),LOGINFO); // test if tempdir exists @@ -88,19 +94,17 @@ bool VoiceFileCreator::createVoiceFile() } if(cindex != -1) { LOG_INFO() << "extracting voice corrections file"; - QTemporaryFile corrfileT; - corrfileT.open(); - QString cfn = corrfileT.fileName(); + QTemporaryFile *corrfileT = new QTemporaryFile(this); + corrfileT->open(); + QString cfn = corrfileT->fileName(); if(z.extractArchive(cfn, QFileInfo(contents.at(cindex)).fileName())) { emit logItem(tr("Extracted voice corrections file from installation"), LOGINFO); - corrFile = &corrfileT; + delete corrFile; + corrFile = corrfileT; } else { // XXX try to fetch an updated file via voicecorrections_url ? - corrfileT.close(); + delete corrfileT; emit logItem(tr("Using internal voice corrections file"), LOGINFO); - QFile corrfile(":/builtin/voice-corrections.txt"); - corrfile.open(QIODevice::ReadOnly); - corrFile = &corrfile; } } if(eindex != -1) { @@ -182,7 +186,7 @@ bool VoiceFileCreator::createVoiceFile() // everything successful, now create the actual voice file. create(); - if (corrFile->isOpen()) + if (corrFile && corrFile->isOpen()) corrFile->close(); return true; } @@ -326,7 +330,7 @@ void VoiceFileCreator::create(void) { TalkGenerator generator(this); // set language for string correction. If not set no correction will be made. - if(useCorrection) + if(useCorrection && corrFile) generator.setLang(m_lang, corrFile); connect(&generator, &TalkGenerator::done, this, &VoiceFileCreator::done); connect(&generator, &TalkGenerator::logItem, this, &VoiceFileCreator::logItem); From a467bfc55fad45c65132fc99fcc4c9b370c4855b Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 4 Aug 2026 17:02:06 -0400 Subject: [PATCH 049/408] FS#13972 - Improve rbutil SAPI5 stability (Alessio Lenzi) The existing code treated any ready-read notification from cscript as proof that synthesis had completed. The SAPI script can emit other output, so the Utility could check for the wave file before the explicit SYNC reply and report that the output file did not exist. The script also used global "On Error Resume Next" without reporting errors from SpFileStream.Open(), SpVoice.Speak(), or SpFileStream.Close(). Several waits had no timeout, allowing the GUI thread to remain blocked indefinitely. The changes: * reports SAPI COM errors to Rockbox Utility; * verifies that SAPI actually created the requested wave file; * waits for the explicit SYNC reply instead of any process output; * applies finite timeouts to vendor queries, synthesis, and shutdown; * terminates a stuck private cscript process safely; * restarts cscript and retries the current string up to three times when a third-party SAPI engine stops responding during a long generation run. Change-Id: I2cf2aefb704353c648bef0c4312f35282ac4e25d --- tools/sapi_voice.vbs | 24 +++++++- utils/rbutilqt/base/ttssapi.cpp | 103 +++++++++++++++++++++++++++----- 2 files changed, 109 insertions(+), 18 deletions(-) diff --git a/tools/sapi_voice.vbs b/tools/sapi_voice.vbs index 371c5c5a23..5bfc820dfa 100644 --- a/tools/sapi_voice.vbs +++ b/tools/sapi_voice.vbs @@ -46,6 +46,7 @@ Dim oVoice ' for traversing the list of voices Dim nLangID, sSelectString Dim aLine, aData ' used in command reading +Dim nError, sError ' error returned to the controlling process On Error Resume Next @@ -146,10 +147,27 @@ Do aData = Split(aLine(1), vbTab, 2) If bVerbose Then WScript.StdErr.WriteLine "Saying " & aData(1) _ & " in " & aData(0) + Err.Clear oSpFS.Open aData(0), SSFMCreateForWrite, false - Set oSpVoice.AudioOutputStream = oSpFS - oSpVoice.Speak aData(1) - oSpFS.Close + nError = Err.Number + sError = Err.Description + If nError = 0 Then + Set oSpVoice.AudioOutputStream = oSpFS + oSpVoice.Speak aData(1) + nError = Err.Number + sError = Err.Description + oSpFS.Close + If nError = 0 And Err.Number <> 0 Then + nError = Err.Number + sError = Err.Description + End If + End If + If nError <> 0 Then + oStdOut.WriteLine "ERROR" & vbTab & nError & ": " & sError + Err.Clear + ElseIf Not oFSO.FileExists(aData(0)) Then + oStdOut.WriteLine "ERROR" & vbTab & "SAPI reported success but created no wave file" + End If Case "EXEC" If bVerbose Then WScript.StdErr.WriteLine "> " & aLine(1) oShell.Run aLine(1), 0, true diff --git a/utils/rbutilqt/base/ttssapi.cpp b/utils/rbutilqt/base/ttssapi.cpp index 14c0cae70e..1ef12cb628 100644 --- a/utils/rbutilqt/base/ttssapi.cpp +++ b/utils/rbutilqt/base/ttssapi.cpp @@ -38,6 +38,8 @@ TTSSapi::TTSSapi(QObject* parent) : TTSBase(parent) m_TTSType = "sapi"; defaultLanguage = "english"; + voicescript = nullptr; + voicestream = nullptr; m_started = false; } @@ -158,15 +160,24 @@ bool TTSSapi::start(QString *errStr) QString TTSSapi::voiceVendor(void) { bool keeprunning = m_started; - QString vendor; + QString vendor = "(unknown)"; if(!m_started) { QString error; - start(&error); + if(!start(&error)) { + LOG_ERROR() << "could not start SAPI while querying vendor:" << error; + return vendor; + } } *voicestream << "QUERY\tVENDOR\r\n"; voicestream->flush(); - while((vendor = voicestream->readLine()).isEmpty()) - QCoreApplication::processEvents(); + if(voicescript->waitForReadyRead(5000)) { + QString response = voicestream->readLine(); + if(!response.isEmpty()) + vendor = response; + } + else { + LOG_ERROR() << "SAPI timed out while querying the voice vendor"; + } LOG_INFO() << "TTS vendor:" << vendor; if(!keeprunning) { @@ -231,31 +242,93 @@ QStringList TTSSapi::getVoiceList(QString language) TTSStatus TTSSapi::voice(const QString& text, const QString& wavfile, QString *errStr) { - (void) errStr; QString query = "SPEAK\t"+wavfile+"\t"+text; LOG_INFO() << "voicing" << query; // append newline to query. Done now to keep debug output more readable. query.append("\r\n"); - *voicestream << query; - *voicestream << "SYNC\tbla\r\n"; - voicestream->flush(); - // do NOT poll the output with readLine(), this causes sync issues! - voicescript->waitForReadyRead(); - if(!QFileInfo(wavfile).isFile()) { - LOG_ERROR() << "output file does not exist:" << wavfile; - return FatalError; + // Some third-party SAPI voices occasionally stop responding after many + // consecutive requests. Restart the private cscript process and retry the + // current string instead of discarding the entire voice-file operation. + constexpr int maxAttempts = 3; + constexpr int responseTimeout = 20000; + for(int attempt = 1; attempt <= maxAttempts; ++attempt) { + QFile::remove(wavfile); + *voicestream << query; + *voicestream << "SYNC\tbla\r\n"; + voicestream->flush(); + + // Wait for the explicit SYNC reply. QProcess::waitForReadyRead() alone + // is insufficient: the script can also write warnings, and older code + // mistook those for completion before the wave file existed. + QElapsedTimer timer; + timer.start(); + bool synced = false; + while(timer.elapsed() < responseTimeout) { + int remaining = responseTimeout - static_cast(timer.elapsed()); + if(!voicescript->waitForReadyRead(remaining)) + break; + + QString response = voicestream->readLine(); + if(response == "bla") { + synced = true; + break; + } + if(response.startsWith("ERROR\t")) { + *errStr = response.mid(6); + LOG_ERROR() << "SAPI error:" << *errStr; + return FatalError; + } + } + + if(synced && QFileInfo(wavfile).isFile()) + return NoError; + + if(synced) { + *errStr = tr("SAPI did not create the output wave file"); + LOG_ERROR() << "output file does not exist:" << wavfile; + return FatalError; + } + + LOG_WARNING() << "SAPI timed out on attempt" << attempt + << "of" << maxAttempts << "for" << text; + if(attempt < maxAttempts) { + stop(); + QString startError; + if(!start(&startError)) { + *errStr = tr("Could not restart SAPI after a timeout: %1") + .arg(startError); + LOG_ERROR() << *errStr; + return FatalError; + } + } } - return NoError; + + *errStr = tr("SAPI timed out repeatedly while generating speech"); + LOG_ERROR() << *errStr; + return FatalError; } bool TTSSapi::stop() { + if(!m_started || voicescript == nullptr) + return true; + *voicestream << "QUIT\r\n"; voicestream->flush(); - voicescript->waitForFinished(); + if(!voicescript->waitForFinished(5000)) { + LOG_WARNING() << "SAPI process did not quit, terminating it"; + voicescript->terminate(); + if(!voicescript->waitForFinished(2000)) { + LOG_WARNING() << "SAPI process did not terminate, killing it"; + voicescript->kill(); + voicescript->waitForFinished(2000); + } + } delete voicestream; delete voicescript; + voicestream = nullptr; + voicescript = nullptr; QFile::setPermissions(QDir::tempPath() +"/sapi_voice.vbs", QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadUser | QFile::WriteUser | QFile::ExeUser From b217a550598e2fc7fbb76924cb6758b18bfeff83 Mon Sep 17 00:00:00 2001 From: Hemant Kumar Date: Tue, 14 Jul 2026 20:35:07 -0600 Subject: [PATCH 050/408] ipod6g: Add inline earphone remote support Decode the jack remote's play/pause and volume buttons via the "Mikey" controller (I2C 0x72) and report them as multimedia keys so they work on every screen, like the OF. Protocol reverse engineered on-device, notes in mikey-6g.c. Change-Id: If5f3d3abf043c0ce0d8ca7beb0f4b591e41c5c43 --- apps/misc.c | 8 + docs/CREDITS | 1 + firmware/SOURCES | 1 + firmware/export/button.h | 6 +- firmware/export/config/ipod6g.h | 2 + firmware/target/arm/ipod/button-clickwheel.c | 17 +- firmware/target/arm/s5l8702/debug-s5l8702.c | 11 + firmware/target/arm/s5l8702/ipod6g/audio-6g.c | 30 +- firmware/target/arm/s5l8702/ipod6g/mikey-6g.c | 342 ++++++++++++++++++ .../target/arm/s5l8702/ipod6g/mikey-target.h | 49 +++ manual/rockbox_interface/main.tex | 8 + 11 files changed, 444 insertions(+), 31 deletions(-) create mode 100644 firmware/target/arm/s5l8702/ipod6g/mikey-6g.c create mode 100644 firmware/target/arm/s5l8702/ipod6g/mikey-target.h diff --git a/apps/misc.c b/apps/misc.c index ea4726d0c2..7aa931636d 100644 --- a/apps/misc.c +++ b/apps/misc.c @@ -809,6 +809,14 @@ long default_event_handler_ex(long event, void (*callback)(void *), void *parame case BUTTON_MULTIMEDIA_STOP: list_stop_handler(); return event; + case BUTTON_MULTIMEDIA_VOLUME_UP: + case BUTTON_MULTIMEDIA_VOLUME_UP|BUTTON_REPEAT: + adjust_volume(1); + return event; + case BUTTON_MULTIMEDIA_VOLUME_DOWN: + case BUTTON_MULTIMEDIA_VOLUME_DOWN|BUTTON_REPEAT: + adjust_volume(-1); + return event; case BUTTON_MULTIMEDIA_REW: case BUTTON_MULTIMEDIA_FFWD: /* not supported yet, needs to be done in the WPS */ diff --git a/docs/CREDITS b/docs/CREDITS index d43382efb5..b7acbb46d6 100644 --- a/docs/CREDITS +++ b/docs/CREDITS @@ -764,6 +764,7 @@ Teun van Dalen Adam N. Burke Lê Khánh Bình Michael McAllister +Hemant Kumar The libmad team The wavpack team diff --git a/firmware/SOURCES b/firmware/SOURCES index 202575595f..3b3a62765d 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1675,6 +1675,7 @@ target/arm/s5l8702/ipod6g/serial-6g.c #ifndef BOOTLOADER target/arm/s5l8702/ipod6g/audio-6g.c target/arm/s5l8702/ipod6g/cscodec-6g.c +target/arm/s5l8702/ipod6g/mikey-6g.c #endif /* BOOTLOADER */ #endif /* IPOD_6G */ diff --git a/firmware/export/button.h b/firmware/export/button.h index dadbf4e5a2..479fd95679 100644 --- a/firmware/export/button.h +++ b/firmware/export/button.h @@ -106,13 +106,17 @@ int button_apply_acceleration(const unsigned int data); #define BUTTON_MULTIMEDIA_NEXT (BUTTON_MULTIMEDIA|0x08) #define BUTTON_MULTIMEDIA_REW (BUTTON_MULTIMEDIA|0x10) #define BUTTON_MULTIMEDIA_FFWD (BUTTON_MULTIMEDIA|0x20) +#define BUTTON_MULTIMEDIA_VOLUME_UP (BUTTON_MULTIMEDIA|0x40) +#define BUTTON_MULTIMEDIA_VOLUME_DOWN (BUTTON_MULTIMEDIA|0x80) #define BUTTON_MULTIMEDIA_ALL (BUTTON_MULTIMEDIA_PLAYPAUSE| \ BUTTON_MULTIMEDIA_STOP| \ BUTTON_MULTIMEDIA_PREV| \ BUTTON_MULTIMEDIA_NEXT| \ BUTTON_MULTIMEDIA_REW | \ - BUTTON_MULTIMEDIA_FFWD) + BUTTON_MULTIMEDIA_FFWD| \ + BUTTON_MULTIMEDIA_VOLUME_UP| \ + BUTTON_MULTIMEDIA_VOLUME_DOWN) #ifdef HAVE_TOUCHSCREEN long touchscreen_last_touch(void); diff --git a/firmware/export/config/ipod6g.h b/firmware/export/config/ipod6g.h index d1cb511bb5..3980eaa483 100644 --- a/firmware/export/config/ipod6g.h +++ b/firmware/export/config/ipod6g.h @@ -262,6 +262,8 @@ #ifndef LOGF_SERIAL #define IPOD_ACCESSORY_PROTOCOL #endif +/* Inline earphone remote, decoded by the jack "Mikey" controller */ +#define HAVE_MIKEY_REMOTE #endif /* Define this if you can switch on/off the accessory power supply */ diff --git a/firmware/target/arm/ipod/button-clickwheel.c b/firmware/target/arm/ipod/button-clickwheel.c index 4c54e556cd..fc10efc9a4 100644 --- a/firmware/target/arm/ipod/button-clickwheel.c +++ b/firmware/target/arm/ipod/button-clickwheel.c @@ -45,6 +45,9 @@ #include "pmu-target.h" #include "clocking-s5l8702.h" #endif +#ifdef HAVE_MIKEY_REMOTE +#include "mikey-target.h" /* inline earphone remote */ +#endif #define WHEEL_FAST_OFF_TIMEOUT 250000 /* timeout for acceleration = 250ms */ #define WHEEL_REPEAT_TIMEOUT 250000 /* timeout for button repeat = 250ms */ @@ -399,6 +402,9 @@ void button_init_device(void) #endif s5l_clickwheel_init(); semaphore_wait(&button_init_wakeup, HZ / 10); +#ifdef HAVE_MIKEY_REMOTE + mikey_init(); /* start inline earphone remote polling */ +#endif } bool button_hold(void) @@ -474,9 +480,14 @@ int button_read_device(void) } /* The int_btn variable is set in the button interrupt handler */ + int btn = int_btn; #ifdef IPOD_ACCESSORY_PROTOCOL - return int_btn | remote_control_rx(); -#else - return int_btn; + btn |= remote_control_rx(); #endif +#ifdef HAVE_MIKEY_REMOTE + /* not gated by the hold switch, matching the OF: the inline remote + * keeps working while the device is locked */ + btn |= mikey_button_read(); +#endif + return btn; } diff --git a/firmware/target/arm/s5l8702/debug-s5l8702.c b/firmware/target/arm/s5l8702/debug-s5l8702.c index 9b58c46dfa..fbf780b20e 100644 --- a/firmware/target/arm/s5l8702/debug-s5l8702.c +++ b/firmware/target/arm/s5l8702/debug-s5l8702.c @@ -35,6 +35,9 @@ #include "uart-target.h" #include "uc87xx.h" #endif +#ifdef HAVE_MIKEY_REMOTE +#include "mikey-target.h" +#endif #include "clocking-s5l8702.h" #define DEBUG_CANCEL BUTTON_MENU @@ -133,6 +136,14 @@ bool dbg_hw_info(void) #ifdef IPOD_ACCESSORY_PROTOCOL _DEBUG_PRINTF("accessory present: %s", pmu_accessory_present() ? "true" : "false"); +#endif +#ifdef HAVE_MIKEY_REMOTE + /* r4/r5 are live register reads to help characterize the + * remote-ID behavior across units (it varies; see + * mikey-6g.c). r4 bit6 = ID bit, r5 = event register. */ + _DEBUG_PRINTF("mikey remote ctrl: %s r4=%02x r5=%02x", + mikey_present() ? "ok" : "--", + mikey_read(4), mikey_read(5)); #endif line++; _DEBUG_PRINTF("ADC:"); diff --git a/firmware/target/arm/s5l8702/ipod6g/audio-6g.c b/firmware/target/arm/s5l8702/ipod6g/audio-6g.c index 9775325cdc..244502f0e0 100644 --- a/firmware/target/arm/s5l8702/ipod6g/audio-6g.c +++ b/firmware/target/arm/s5l8702/ipod6g/audio-6g.c @@ -23,34 +23,10 @@ #include "sound.h" #include "pmu-target.h" #include "i2c-s5l8702.h" +#include "mikey-target.h" extern int rec_hw_ver; -/* Mikey is the internal controller for jack microphone and/or - * remote accessories. - * TODO: - * - move to mikey-6g.c - * - detect jack accessory - * - support for remote buttons - */ -unsigned char mikey_read(int address) -{ - unsigned char val; - i2c_read(0, 0x72, address, 1, &val); - return val; -} - -int mikey_write(int address, unsigned char val) -{ - return i2c_write(0, 0x72, address, 1, &val); -} - -void mikey_reset(void) -{ - mikey_write(0, 5); - mikey_write(1, 0x80); -} - #if INPUT_SRC_CAPS != 0 #ifdef HAVE_RECORDING void audio_enable_mic(bool enable) @@ -60,12 +36,12 @@ void audio_enable_mic(bool enable) if (enable) { - mikey_write(0, 7); /* raise voltage to polarize microphone */ + mikey_set_mic_capture(true); /* polarize mic, pause the remote */ GPIOCMD = 0xe070f; /* enable preamp */ } else { - mikey_reset(); /* microphone line voltage = 0 */ + mikey_set_mic_capture(false); GPIOCMD = 0xe070e; /* disable preamp */ } } diff --git a/firmware/target/arm/s5l8702/ipod6g/mikey-6g.c b/firmware/target/arm/s5l8702/ipod6g/mikey-6g.c new file mode 100644 index 0000000000..8165e51469 --- /dev/null +++ b/firmware/target/arm/s5l8702/ipod6g/mikey-6g.c @@ -0,0 +1,342 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Hemant Kumar + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* "Mikey" is the internal controller for the headphone jack microphone and + * the inline earphone remote (I2C bus 0, address 0x72). + * + * Register protocol (reverse engineered on-device, 2026-07): + * reg0 = 0x2f remote-reporting mode. The low 3 bits are the mic bias + * level (7, same as the recording path uses); 0x28 arms + * the button detection engine. The chip resets on jack + * removal (all registers read zero afterwards), so the + * mode is re-armed on insertion. + * reg4 & 0x40 identified-remote bit, set after the chip hears an + * Apple remote's ID chirp. UNIT-DEPENDENT: reliably + * latched on one 6G, never observed on two other + * 120GB units, so nothing is gated on it; it is only + * shown in the hardware debug screen. + * reg4 & 0x05 center (play/pause) held; a level signal. Can + * flicker while the mode is (re)armed, hence the + * "armed" guard below. + * reg5 volume edge events, each readable for ~100ms: + * 0x04 vol+ press, 0x08 vol+ release, + * 0x01 vol- press, 0x02 vol- release. + * 0x30 is the accessory-ID event (no button meaning). + * The remote itself signals volume as static DC loads on the mic line and + * identifies via a one-time ultrasonic chirp (the same scheme David Carne + * documented for the shuffle 3G remote); Mikey does that level and chirp + * detection in hardware and reports the results as above. */ + +#include "system.h" +#include "cpu.h" +#include "kernel.h" +#include "thread.h" +#include "button.h" +#include "i2c-s5l8702.h" +#include "mikey-target.h" + +#define MIKEY_ADDR 0x72 +#define MIKEY_REG_MODE 0 /* mic bias + detection-engine mode */ +#define MIKEY_REG_BTN 4 /* center-button level */ +#define MIKEY_REG_EVT 5 /* volume press/release edge events */ + +#define MIKEY_MODE_OFF 0x05 /* reset default, mic line unpowered */ +#define MIKEY_MODE_MIC 0x07 /* mic bias raised, for recording */ +#define MIKEY_MODE_REMOTE 0x2f /* mic bias + button detection */ + +#define MIKEY_ID_FLAG 0x40 /* identified-remote latch in reg4 */ +#define MIKEY_BTN_CENTER 0x05 +#define MIKEY_EVT_VOLUP_DN 0x04 +#define MIKEY_EVT_VOLUP_UP 0x08 +#define MIKEY_EVT_VOLDN_DN 0x01 +#define MIKEY_EVT_VOLDN_UP 0x02 + +/* A press+release pair landing in a single poll is stretched over this + * many polls, so the button driver's debounce (two identical consecutive + * reads at the 10ms button tick) still accepts the click. */ +#define MIKEY_CLICK_POLLS 2 + +/* Center-button handling: click-only. reg4's rising edge is always + * immediate, but after ~5s of button inactivity the chip's event engine + * naps and reports the release (reg4 fall, reg6 events) ~1.8s late, so + * the press duration is unreliable. Every press is therefore reported + * as a fixed-length click at the rise; there is no hold/long-press. A + * release must be seen for several consecutive polls before a new rise + * counts, so bounce or an I2C NAK can't double-fire a click. */ +#define MIKEY_CENTER_PULSE_POLLS 3 /* reported click length, 60ms */ +#define MIKEY_CENTER_OFF_POLLS 3 /* release debounce, 60ms */ + +unsigned char mikey_read(int address) +{ + /* default to "no press, no events" if the transfer fails: the chip + * NAKs while it resets itself around jack removal/insertion */ + unsigned char val = 0; + i2c_read(0, MIKEY_ADDR, address, 1, &val); + return val; +} + +int mikey_write(int address, unsigned char val) +{ + return i2c_write(0, MIKEY_ADDR, address, 1, &val); +} + +void mikey_reset(void) +{ + mikey_write(MIKEY_REG_MODE, MIKEY_MODE_OFF); + mikey_write(1, 0x80); +} + +static int mikey_btn = BUTTON_NONE; +static volatile bool mic_active = false; +static bool mikey_detected = false; +static long mikey_stack[DEFAULT_STACK_SIZE/2/sizeof(long)]; + +bool mikey_present(void) +{ + return mikey_detected; +} + +/* The recording path owns the chip while the jack mic is enabled; the + * polling thread backs off and re-arms the remote mode afterwards. */ +void mikey_set_mic_capture(bool enable) +{ + mic_active = enable; + if (enable) + mikey_write(MIKEY_REG_MODE, MIKEY_MODE_MIC); + else + mikey_reset(); +} + +/* Decode one volume button's press/release edge events into a held state. + * A real hold only produces the two edges, so between them *held carries + * the state; *click stretches a same-poll press+release (see above). */ +static void mikey_decode_vol(unsigned char evt, unsigned char dn, + unsigned char up, bool *held, int *click) +{ + if (*click > 0 && --*click == 0) + *held = false; + + if (evt & dn) + { + *held = true; + *click = (evt & up) ? MIKEY_CLICK_POLLS : 0; + } + else if ((evt & up) && *click == 0) + *held = false; +} + +/* Phantom-load failsafe: some passive cables present a DC load on the + * mic line that Mikey decodes as a volume button, producing a press + * edge the moment the detection engine arms and never a release (seen + * in the wild as volume ramping to zero on its own). A press edge this + * early cannot be a human, so it suppresses that button until a + * release edge proves a real remote is attached. This is the + * edge-event analog of the center button's "armed" guard, which + * already keeps mic-less TRS plugs (a permanent mic-line short) from + * reading as a stuck center button. */ +#define MIKEY_ARM_WINDOW_POLLS 10 /* 200ms after (re)arming */ + +/* Per-poll decode state, kept apart from the thread's power/mode + * bookkeeping so the register-to-button logic is a pure function of + * (state, reg4, reg5). */ +struct mikey_decode { + bool armed; /* need a released reading before first press */ + bool vol_up, vol_dn; + int up_click, dn_click; + int arm_window; /* polls left of phantom-press suppression window */ + bool up_suppressed, dn_suppressed; + bool center_down; /* debounced "press already reported" */ + int center_off; /* consecutive released polls */ + int center_pulse; /* click pulse countdown */ +}; + +static void mikey_decode_reset(struct mikey_decode *d) +{ + d->armed = false; + d->vol_up = d->vol_dn = false; + d->up_click = d->dn_click = 0; + d->arm_window = MIKEY_ARM_WINDOW_POLLS; + d->up_suppressed = d->dn_suppressed = false; + d->center_down = false; + d->center_off = MIKEY_CENTER_OFF_POLLS; + d->center_pulse = 0; +} + +/* Apply the phantom-load failsafe to one volume button's edge bits: + * a press edge (without a same-poll release) inside the arm window + * marks the button suppressed; suppressed press edges are stripped + * until a release edge lifts the suppression. */ +static unsigned char mikey_vol_gate(struct mikey_decode *d, unsigned char evt, + unsigned char dn, unsigned char up, + bool *suppressed) +{ + if (evt & up) + *suppressed = false; + else if ((evt & dn) && d->arm_window > 0) + *suppressed = true; + + if (*suppressed) + evt &= ~dn; + return evt; +} + +/* Decode one poll's register pair into a button mask. Volume is NOT + * gated on the reg4 identified-remote bit: that bit proved to be + * unit-dependent (reliably latched on one 6G, never seen on two other + * 120GB units where gating on it broke volume entirely), so it is + * only surfaced in the hardware debug screen for now. */ +static int mikey_decode_poll(struct mikey_decode *d, + unsigned char btn, unsigned char evt) +{ + evt = mikey_vol_gate(d, evt, MIKEY_EVT_VOLUP_DN, MIKEY_EVT_VOLUP_UP, + &d->up_suppressed); + evt = mikey_vol_gate(d, evt, MIKEY_EVT_VOLDN_DN, MIKEY_EVT_VOLDN_UP, + &d->dn_suppressed); + if (d->arm_window > 0) + d->arm_window--; + + mikey_decode_vol(evt, MIKEY_EVT_VOLUP_DN, MIKEY_EVT_VOLUP_UP, + &d->vol_up, &d->up_click); + mikey_decode_vol(evt, MIKEY_EVT_VOLDN_DN, MIKEY_EVT_VOLDN_UP, + &d->vol_dn, &d->dn_click); + + /* reg4 can read pressed while the mode settles; don't report the + * center button until it has been seen released once */ + bool raw = (btn & MIKEY_BTN_CENTER) != 0; + if (!raw) + d->armed = true; + + /* click-only: emit a fixed pulse at each debounced rise */ + if (raw && d->armed) + { + if (!d->center_down && d->center_off >= MIKEY_CENTER_OFF_POLLS) + d->center_pulse = MIKEY_CENTER_PULSE_POLLS; + d->center_down = true; + d->center_off = 0; + } + else + { + if (d->center_off < MIKEY_CENTER_OFF_POLLS) + d->center_off++; + if (d->center_off >= MIKEY_CENTER_OFF_POLLS) + d->center_down = false; + } + + bool center = false; + if (d->center_pulse > 0) + { + center = true; + d->center_pulse--; + } + + /* All buttons report as multimedia keys: handled globally by + * default_event_handler on every screen, so volume and play/pause + * work in menus, WPS and plugins alike, matching the OF. */ + return (center ? BUTTON_MULTIMEDIA_PLAYPAUSE : 0) + | (d->vol_up ? BUTTON_MULTIMEDIA_VOLUME_UP : 0) + | (d->vol_dn ? BUTTON_MULTIMEDIA_VOLUME_DOWN : 0); +} + +/* The chip only ACKs the bus while something occupies the jack, so + * presence can't be probed at boot; it is probed here whenever the + * jack is occupied. A few insertion transients may NAK before the + * first ACK; a model without the chip (2007) NAKs forever, so after + * the retry budget the poll backs way off to stay out of the way. */ +#define MIKEY_PROBE_TRIES 25 /* ~2.5s of HZ/10 retries */ + +static void mikey_thread(void) +{ + bool powered = false; + int naks = 0; + struct mikey_decode decode; + + mikey_decode_reset(&decode); + + while (1) + { + if (!headphones_inserted() || mic_active) + { + if (powered && !mic_active) + mikey_reset(); + powered = false; + naks = 0; + mikey_decode_reset(&decode); + mikey_btn = BUTTON_NONE; + sleep(HZ/2); /* nothing to poll, check back at leisure */ + continue; + } + + /* Re-arm when the mode readback disagrees (first pass, chip reset + * on jack removal, or the recording path rewrote it). Arming can + * flicker reg4 and emits a spurious accessory-ID event, so drop + * the state and settle for one poll. */ + unsigned char mode; + if (i2c_read(0, MIKEY_ADDR, MIKEY_REG_MODE, 1, &mode) != 0) + { + /* no ACK: chip resetting around insertion, or not present */ + mikey_decode_reset(&decode); + mikey_btn = BUTTON_NONE; + if (naks < MIKEY_PROBE_TRIES) + { + naks++; + sleep(HZ/10); + } + else + sleep(HZ*2); + continue; + } + naks = 0; + powered = true; + mikey_detected = true; /* latched for the hw debug screen */ + + if (mode != MIKEY_MODE_REMOTE) + { + mikey_write(MIKEY_REG_MODE, MIKEY_MODE_REMOTE); + mikey_decode_reset(&decode); + mikey_btn = BUTTON_NONE; + sleep(HZ/50); + continue; + } + + unsigned char evt = mikey_read(MIKEY_REG_EVT); + unsigned char btn = mikey_read(MIKEY_REG_BTN); + mikey_btn = mikey_decode_poll(&decode, btn, evt); + + /* volume edge events stay readable for ~100ms; 20ms leaves a + * comfortable margin against scheduling jitter */ + sleep(HZ/50); + } +} + +int mikey_button_read(void) +{ + return mikey_btn; +} + +void mikey_init(void) +{ + /* Presence can't be probed here: the chip only ACKs while the + * jack is occupied (booting without headphones must not disable + * the remote). The thread probes on insertion instead and backs + * off on models without the chip (2007). */ + mikey_reset(); + create_thread(mikey_thread, mikey_stack, sizeof(mikey_stack), 0, + "mikey" IF_PRIO(, PRIORITY_SYSTEM) IF_COP(, CPU)); +} diff --git a/firmware/target/arm/s5l8702/ipod6g/mikey-target.h b/firmware/target/arm/s5l8702/ipod6g/mikey-target.h new file mode 100644 index 0000000000..888c14cde6 --- /dev/null +++ b/firmware/target/arm/s5l8702/ipod6g/mikey-target.h @@ -0,0 +1,49 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Hemant Kumar + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef __MIKEY_TARGET_H__ +#define __MIKEY_TARGET_H__ + +/* "Mikey" is the internal controller for the headphone jack microphone + * and inline remote (I2C bus 0, address 0x72). */ + +/* Low-level register access. */ +unsigned char mikey_read(int address); +int mikey_write(int address, unsigned char val); +void mikey_reset(void); + +/* Recording-path handover: while the jack mic is enabled the remote + * polling backs off and Mikey stays in plain mic-bias mode. */ +void mikey_set_mic_capture(bool enable); + +/* Inline-remote button support. mikey_init() starts a polling thread; + * mikey_button_read() returns the current button mask (multimedia key + * codes, handled globally by default_event_handler: PLAYPAUSE for the + * center click, VOLUME_UP/DOWN for volume; BUTTON_NONE if nothing + * pressed) and is safe to call from the button tick. */ +void mikey_init(void); +int mikey_button_read(void); + +/* True once the controller has ever ACKed the bus (it only responds + * while the jack is occupied, so this latches on first insertion; + * stays false on the early 2007 models, which lack the chip). Shown + * in the hardware debug screen. */ +bool mikey_present(void); + +#endif /* __MIKEY_TARGET_H__ */ diff --git a/manual/rockbox_interface/main.tex b/manual/rockbox_interface/main.tex index 5b5c418792..594e261401 100644 --- a/manual/rockbox_interface/main.tex +++ b/manual/rockbox_interface/main.tex @@ -176,6 +176,14 @@ detail in the following paragraph. \ButtonHold{} is off before trying to use your player. } % + \opt{ipod6g}{ + Earphones with an Apple-compatible inline remote are supported (not on + the 2007 model). A click of the remote's center button toggles + play/pause on any screen, even while the \ButtonHold{} switch is on; + the center button has no long-press function. The remote's volume + buttons adjust the \dap{}'s volume everywhere. + } + % \opt{ipod3g}{ The main controls on the \dap{} are a slightly indented touch wheel with a flat round button in the center, and four buttons in a row above the From 20c763ff891b145f438aad8febeb324ce7a67965 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Wed, 5 Aug 2026 09:19:26 -0400 Subject: [PATCH 051/408] FS#13970 - lcd_drawline() different depending on drawing direction swap x1/x2 and y1/y2 rather than negating the increment Change-Id: I16c37ab00c2a9a7f97768da68508c2d5c98a14c5 --- firmware/drivers/lcd-bitmap-common.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/firmware/drivers/lcd-bitmap-common.c b/firmware/drivers/lcd-bitmap-common.c index 68299df246..c4009155fa 100644 --- a/firmware/drivers/lcd-bitmap-common.c +++ b/firmware/drivers/lcd-bitmap-common.c @@ -925,14 +925,18 @@ void LCDFN(drawline)(int x1, int y1, int x2, int y2) if (x1 > x2) { - xinc1 = -xinc1; - xinc2 = -xinc2; + /* swap their identities */ + int x0 = x2; + x2 = x1; + x1 = x0; } if (y1 > y2) { - yinc1 = -yinc1; - yinc2 = -yinc2; + /* swap their identities */ + int y0 = y2; + y2 = y1; + y1 = y0; } x = x1; From dcdb539ca54e90700d4a6ce20350e34f700a96f7 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Wed, 5 Aug 2026 11:40:02 -0400 Subject: [PATCH 052/408] FS#13970 - lcd_drawline() different depending on drawing direction try#2 Last patch didn't swap both x and y so was wrong and messed up the coord pair I didn't want to add more swapping due to extra overhead ultimately the issue is that you have 1/2 pixel error (-1 0 1) and when doing the reverse line the error is on the wrong side so instead get the first point from the opposite dinc Change-Id: I141c4af36a601314f2b509a9ebfcfab3b5213bcd --- firmware/drivers/lcd-bitmap-common.c | 40 +++++++++++++++++----------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/firmware/drivers/lcd-bitmap-common.c b/firmware/drivers/lcd-bitmap-common.c index c4009155fa..e1ebd7267b 100644 --- a/firmware/drivers/lcd-bitmap-common.c +++ b/firmware/drivers/lcd-bitmap-common.c @@ -911,6 +911,18 @@ void LCDFN(drawline)(int x1, int y1, int x2, int y2) dinc2 = (deltay - deltax) * 2; xinc1 = 1; yinc1 = 0; + if (x1 > x2) + { + xinc1 = -xinc1; + xinc2 = -xinc2; + } + + if (y1 > y2) + { + d += dinc2; + yinc1 = -yinc1; + yinc2 = -yinc2; + } } else { @@ -920,25 +932,21 @@ void LCDFN(drawline)(int x1, int y1, int x2, int y2) dinc2 = (deltax - deltay) * 2; xinc1 = 0; yinc1 = 1; + if (x1 > x2) + { + d += dinc2; + xinc1 = -xinc1; + xinc2 = -xinc2; + } + + if (y1 > y2) + { + yinc1 = -yinc1; + yinc2 = -yinc2; + } } numpixels++; /* include endpoints */ - if (x1 > x2) - { - /* swap their identities */ - int x0 = x2; - x2 = x1; - x1 = x0; - } - - if (y1 > y2) - { - /* swap their identities */ - int y0 = y2; - y2 = y1; - y1 = y0; - } - x = x1; y = y1; From 290b06c86952d05e32c6ac2b0cfea71880764b51 Mon Sep 17 00:00:00 2001 From: Skye Date: Wed, 5 Aug 2026 23:34:39 +0900 Subject: [PATCH 053/408] plugins/fft: do not starve other threads Change-Id: If8149086ed6e946d300ecdecf1f47dcf294822cb --- apps/plugins/fft/fft.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/plugins/fft/fft.c b/apps/plugins/fft/fft.c index 18aefe2503..52a3d277fa 100644 --- a/apps/plugins/fft/fft.c +++ b/apps/plugins/fft/fft.c @@ -1284,7 +1284,7 @@ enum plugin_status plugin_start(const void* parameter) rb->yield(); /* tmo = 0 won't yield */ } - int button = pluginlib_getaction(TIMEOUT_NOBLOCK, plugin_contexts, ARRAYLEN(plugin_contexts)); + int button = pluginlib_getaction(delay, plugin_contexts, ARRAYLEN(plugin_contexts)); switch (button) { From 20f4f9539a8b56b60227eef9e23c9b5170d9078b Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 30 Jul 2026 22:09:31 +0300 Subject: [PATCH 054/408] hiby: usb dac: fix crackling sound due to sample rate mismatch Both Ubuntu and Windows fail to report the proper sample rate on my machines, causing crackling sound on my hiby r1. Since we explicitly specify the only supported sample rate (48 kHz), just ignore the reported value. Change-Id: Ie6e85bf94f9e15fca0c968441d5a8ce5a0088b77 --- firmware/target/hosted/hiby/usb-dac-hiby.c | 22 ++++++++++++------- firmware/target/hosted/hiby/usb-dac-hiby.h | 2 ++ firmware/target/hosted/hiby/usb-hiby-gadget.c | 4 ++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/firmware/target/hosted/hiby/usb-dac-hiby.c b/firmware/target/hosted/hiby/usb-dac-hiby.c index 4b641a2314..469dc640b9 100644 --- a/firmware/target/hosted/hiby/usb-dac-hiby.c +++ b/firmware/target/hosted/hiby/usb-dac-hiby.c @@ -148,8 +148,6 @@ static void *dac_pump_thread(void *arg) bool usb_dac_start(void) { static const struct mixer_play_cbs cbs = { .get_more = dac_get_more }; - int st[3] = {0, 0, 0}; - unsigned int rate; /* The node is created asynchronously when the UDC binds; during boot * the hotplug helper can lag behind, so wait briefly for it. This runs @@ -168,12 +166,20 @@ bool usb_dac_start(void) return false; } - /* Play at the rate the host negotiated (reported by the kernel). */ - ioctl(dac_fd, UAC_SA_GET_STATUS, st); - if (st[1] >= SAMPR_8 && st[1] <= SAMPR_192) - rate = (unsigned int)st[1]; - else - rate = SAMPR_44; + int rate = USB_DAC_SAMPLE_RATE; +#ifdef LOGF_ENABLE + // Both on Windows and Linux it always returns 44100 instead of requested rate (which indicates some generic issue). + // But since we hardcoded the only supported rate with USB_DAC_SAMPLE_RATE in gadget config (see usb-hiby-gadget.c), + // it should be safe to ignore the mismatch and just log it for debugging purposes. + int st[3] = {0, 0, 0}; + int status_ok = ioctl(dac_fd, UAC_SA_GET_STATUS, st); + int reported_rate = st[1]; + if (reported_rate && reported_rate != rate) + { + logf("uac_sa rate mismatch: ioctl %d, reported %d, expected %d", + status_ok, reported_rate, rate); + } +#endif mixer_set_frequency(rate); dac_head = dac_tail = 0; diff --git a/firmware/target/hosted/hiby/usb-dac-hiby.h b/firmware/target/hosted/hiby/usb-dac-hiby.h index 0e1c85d0a9..2d6f15007a 100644 --- a/firmware/target/hosted/hiby/usb-dac-hiby.h +++ b/firmware/target/hosted/hiby/usb-dac-hiby.h @@ -20,6 +20,8 @@ #ifndef __USB_DAC_HIBY_H__ #define __USB_DAC_HIBY_H__ +#define USB_DAC_SAMPLE_RATE SAMPR_48 + /** * Host-PCM pump for the HiBy USB DAC gadget mode: drains /dev/uac_sa * into the PCM mixer. Started/stopped by the USB gadget driver when the diff --git a/firmware/target/hosted/hiby/usb-hiby-gadget.c b/firmware/target/hosted/hiby/usb-hiby-gadget.c index 5f9f055c0c..c252f6d64e 100644 --- a/firmware/target/hosted/hiby/usb-hiby-gadget.c +++ b/firmware/target/hosted/hiby/usb-hiby-gadget.c @@ -159,7 +159,7 @@ static bool adb_compose_dac(void) system("mkdir -p " USB_GADGET_PATH "/functions/uac_sa.a"); sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_chmask", 3); sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_ssize", 2); - sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_srate", SAMPR_48); + sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_srate", USB_DAC_SAMPLE_RATE); system("ln -sf " USB_GADGET_PATH "/functions/uac_sa.a " USB_GADGET_PATH "/configs/c.1/"); sysfs_set_string(USB_GADGET_PATH "/configs/c.1/strings/0x409/configuration", "uac,adb"); return true; @@ -422,7 +422,7 @@ bool enable_usb_audio(void) system("mkdir -p " USB_GADGET_PATH "/functions/uac_sa.a"); sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_chmask", 3); sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_ssize", 2); - sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_srate", SAMPR_48); + sysfs_set_int(USB_GADGET_PATH "/functions/uac_sa.a/c_srate", USB_DAC_SAMPLE_RATE); /* Present an IAD audio device with the HiBy DAC VID/PID so the host * recognises it as a USB DAC. */ From 2d2b03d314455112afa7dd8d369874ae0233a3e1 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sat, 1 Aug 2026 08:53:44 -0400 Subject: [PATCH 055/408] build: Bundle the main .map files into the zip file This covers the rockbox binary (or bootloader), which increases the zip file by 60-80k depending on the target. It currently *excludes* codecs and plugins, which can add over 1MB to the zip file. We can revisit this when/if we move to distributing 7zip files instead. Change-Id: Iec72dd3aa634ceca716975c13b7fa8707a485d30 --- tools/buildzip.pl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/buildzip.pl b/tools/buildzip.pl index e10396dac7..0613be1484 100755 --- a/tools/buildzip.pl +++ b/tools/buildzip.pl @@ -692,6 +692,13 @@ sub buildzip { # copy the .lua files glob_mkdir("$temp_dir/rocks/viewers/lua/"); glob_copy('apps/plugins/lua/*.lua', "$temp_dir/rocks/viewers/lua/"); + + # finally, bundle map files for debug + glob_mkdir("$temp_dir/debug"); + glob_copy("*.map", "$temp_dir/debug"); +# codecs and plugin maps add up +# find(find_copyfile(qr/\.(map)/, abs_path("$temp_dir/debug")), 'lib'); +# find(find_copyfile(qr/\.(map)/, abs_path("$temp_dir/debug")), 'apps/plugins'); } my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = From 21d48d5ae3ea84dd7fc8941789ebb739a7c4deed Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 31 Jul 2026 11:20:47 -0400 Subject: [PATCH 056/408] iap: Improve IAPGeneralCommandID_RequestIPodName iap-serial was hardcoded to return "ROCKBOX", and it was not implemented at all in iap-usb. Extend the iap-serial implementation to return the contents of /playername.txt, or "Rockbox" if there is an error opening/reading that file. And cut-n-paste this into the iap-usb side of things Change-Id: I9d328be390b4cc8bd92237f996c41ef1e5e19724 --- apps/iap/iap-lingo4.c | 11 +++++++++-- firmware/usbstack/iap/libiap/iap.c | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/apps/iap/iap-lingo4.c b/apps/iap/iap-lingo4.c index 746d59f397..d29f4d1612 100644 --- a/apps/iap/iap-lingo4.c +++ b/apps/iap/iap-lingo4.c @@ -16,6 +16,7 @@ * ******************************************************************************/ +#include "misc.h" #include "iap-core.h" #include "iap-lingo.h" #include "dir.h" @@ -1034,12 +1035,18 @@ void iap_handlepkt_mode4(const unsigned int len, const unsigned char *buf) * 5 0x14 Command ID (bits 7:0) * 6 0xE5 Telegram payload checksum byte * - * We return ROCKBOX, should this be definable? * Should it be Volume Name? */ { + char ipod_name[32] = "Rockbox"; + int fd = open_utf8(ROCKBOX_DIR "/playername.txt", O_RDONLY); + if (fd >= 0) { + read_line(fd, ipod_name, sizeof(ipod_name)); + close(fd); + ipod_name[sizeof(ipod_name)-1] = 0; + } IAP_TX_INIT4(0x04, 0x0015); - IAP_TX_PUT_STRING("ROCKBOX"); + IAP_TX_PUT_STRING(ipod_name); iap_send_tx(); break; } diff --git a/firmware/usbstack/iap/libiap/iap.c b/firmware/usbstack/iap/libiap/iap.c index b5f700b0a2..af1fe2545c 100644 --- a/firmware/usbstack/iap/libiap/iap.c +++ b/firmware/usbstack/iap/libiap/iap.c @@ -1,6 +1,8 @@ #include #include +#include "misc.h" + #include "constants.h" #include "endian.h" #include "iap.h" @@ -201,6 +203,25 @@ static int32_t handle_command(struct IAPContext* ctx, uint8_t lingo, uint16_t co response->is_ext_mode = 1; return IAPGeneralCommandID_ReturnExtendedInterfaceMode; } break; + case IAPGeneralCommandID_RequestIPodName: { + // Note: This should be pulled from .rockbox/playername.txt + char ipod_name[32] = "Rockbox"; + int fd = open_utf8(ROCKBOX_DIR "/playername.txt", O_RDONLY); + if (fd >= 0) { + read_line(fd, ipod_name, sizeof(ipod_name)); + close(fd); + ipod_name[sizeof(ipod_name)-1] = 0; + } + + check_ret( + iap_span_append(response_span, + ipod_name, + strlen(ipod_name) + 1), + -IAPAckStatus_EOutOfResource + ); + + return IAPGeneralCommandID_ReturnIPodName; + } break; case IAPGeneralCommandID_RequestIPodSoftwareVersion: { alloc_response(IAPReturnIPodSoftwareVersionPayload); response->major = 18; From f44bf5c66d50d2cf9f6cbb54ee3d38531bb0b6c2 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 7 Aug 2026 08:47:38 -0400 Subject: [PATCH 057/408] FS#13974 - Updated Russian translation (Ivan Romaniuk-Mikhailovsky) Change-Id: Ie5d42c11e1811beb486ee12c0b7cd2cdf2703d38 --- apps/lang/russian.lang | 186 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 163 insertions(+), 23 deletions(-) diff --git a/apps/lang/russian.lang b/apps/lang/russian.lang index 47feb49e30..6d4e854d1a 100644 --- a/apps/lang/russian.lang +++ b/apps/lang/russian.lang @@ -593,10 +593,10 @@ *: "Save .cfg File" - *: "Запись профиля настроек" + *: "Сохранить профиль настроек" - *: "Запись профиля настроек" + *: "Сохранить профиль настроек" @@ -733,7 +733,7 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" *: "Полосовой фильтр %d" @@ -14646,20 +14646,6 @@ *: "Не плагин" - - id: LANG_OPEN_PLUGIN_SET_WPS_CONTEXT_PLUGIN - desc: open plugin module - user: core - - *: "Set WPS Context Plugin" - - - *: "Назначить плагин режима воспроизведения" - - - *: "Назначить плагин режима воспроизведения" - - id: LANG_PARAMETER desc: @@ -15846,16 +15832,16 @@ id: LANG_DEFAULT_BROWSER - desc: in Settings + desc: deprecated user: core - *: "Default Browser" + *: "" - *: "Браузер по умолчанию" + *: "" - *: "Браузер по умолчанию" + *: "" @@ -16951,10 +16937,10 @@ *: "Announce Status" - *: "Объявлять статус" + *: "Объявление статуса" - *: "Объявлять статус" + *: "Объявление статуса" @@ -17002,3 +16988,157 @@ *: "Ю С Б" + + id: LANG_SET_CONTEXT_ITEM + desc: WPS context item + user: core + + *: "Set Context Item" + + + *: "Выбрать пункт меню" + + + *: "Выбрать пункт меню" + + + + id: LANG_FILE_NOT_FOUND + desc: When file does not exist + user: core + + *: "File not found" + + + *: "Файл не найден" + + + *: "Файл не найден" + + + + id: LANG_SHOW_IN_FILES + desc: Reveal item in File Browser + user: core + + *: "Show in Files" + + + *: "Показать в файлах" + + + *: "Показать в файлах" + + + + id: LANG_CHANNEL_SWAP + desc: in sound_settings + user: core + + *: "Swap Left & Right" + + + *: "Поменять местами" + + + *: "Поменять местами" + + + + id: LANG_COUNTDOWN_TIMER_SET + desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration + user: core + + *: "SET TIMER" + + + *: "УСТАНОВИТЬ ТАЙМЕР" + + + *: "Установить таймер" + + + + id: LANG_COUNTDOWN_TIMER_RUNNING + desc: countdown_timer plugin - status label shown while the countdown is active + user: core + + *: "RUNNING" + + + *: "РАБОТАЕТ" + + + *: "Работает" + + + + id: LANG_COUNTDOWN_TIMER_PAUSED + desc: countdown_timer plugin - status label shown while the countdown is paused + user: core + + *: "PAUSED" + + + *: "ПАУЗА" + + + *: "Пауза" + + + + id: LANG_COUNTDOWN_TIMER_OVERTIME + desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up + user: core + + *: "OVERTIME" + + + *: "ВРЕМЯ ПОСЛЕ СРАБАТЫВАНИЯ" + + + *: "Время после срабатывания" + + + + id: LANG_COUNTDOWN_TIMER_FINISHED + desc: countdown_timer plugin - status label shown at the moment the countdown expires + user: core + + *: "FINISHED" + + + *: "ВРЕМЯ ВЫШЛО" + + + *: "Время вышло" + + + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Фильтр %d" + + + *: "Фильтр" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Тип фильтра" + + + *: "Тип фильтра" + + From 49600dd77c9ee7195f34a139efe76a294b544e30 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Sun, 2 Aug 2026 23:27:58 -0400 Subject: [PATCH 058/408] filebrowse.lua add sort by type, add .bmp, .mod to known filetypes also extends date and size examples to show the size and timestamp respectively Change-Id: Ife2b74a8a60a6da097f1f714ebaf68a60d504287 --- apps/filetypes.c | 2 + apps/plugins/lua/include_lua/filebrowse.lua | 47 +++++++++++++------ apps/plugins/lua/include_lua/get_files.lua | 19 +++++++- apps/plugins/lua_scripts/file_browser.lua | 50 ++++++++++++++++++--- 4 files changed, 98 insertions(+), 20 deletions(-) diff --git a/apps/filetypes.c b/apps/filetypes.c index 30a1582da8..4c68fc9579 100644 --- a/apps/filetypes.c +++ b/apps/filetypes.c @@ -72,6 +72,8 @@ static const struct filetype_inbuilt inbuilt_filetypes[] = { { "m3u8", FILE_ATTR_M3U }, { "cfg", FILE_ATTR_CFG }, { "wps", FILE_ATTR_WPS }, + { "bmp", FILE_ATTR_BMP }, + { "mod", FILE_ATTR_MOD }, #ifdef HAVE_REMOTE_LCD { "rwps", FILE_ATTR_RWPS }, #endif diff --git a/apps/plugins/lua/include_lua/filebrowse.lua b/apps/plugins/lua/include_lua/filebrowse.lua index c41598fd63..d155074e29 100755 --- a/apps/plugins/lua/include_lua/filebrowse.lua +++ b/apps/plugins/lua/include_lua/filebrowse.lua @@ -29,9 +29,10 @@ local tmploader = require("temploader") -------------------------------------------------------------------------------- -- uses print_table and get_files to display simple file browser --- sort_by "date" "name" "size" +-- sort_by "date" "name" "size" "type" -- descending true/false -function file_choose(dir, title, sort_by, descending) +-- returns selected file and attribs string if get_attr == true +function file_choose(dir, title, sort_by, descending, get_attr) local dstr, hstr = "" if not title then dstr = "%d items found in %0d.%02d seconds" @@ -58,6 +59,7 @@ function file_choose(dir, title, sort_by, descending) local timer local files = {} local dirs = {} + local attrs = {} local item = 1 local cancel_fn = nil @@ -82,20 +84,37 @@ function file_choose(dir, title, sort_by, descending) rb.splash(1, "Searching for Files") dirs, files = get_files(dir, recurse, f_finddir, f_findfile, sort_by, cancel_fn, dirs, files) + local ndirs = #dirs local parentdir = dirs[1] - for i = 1, #dirs do + for i = 1, ndirs do dirs[i] = "\t" .. dirs[i] end - if not descending then - for i = 1, #files do - -- only store file name .. strip attributes from end and parent from beginning - table.insert(dirs, "\t" .. string.match(string.match(files[i], "[^/\\]+$") or "?", "[^;]+") or "?") + if not get_attr then + if not descending then + for i = 1, #files do + -- only store file name .. strip attributes from end and parent from beginning + table.insert(dirs, "\t" .. string.match(string.match(files[i], "[^/\\]+$") or "?", "[^;]+") or "?") + end + else + for i = #files, 1, -1 do + -- only store file name .. strip attributes from end and parent from beginning + table.insert(dirs, "\t" .. string.match(string.match(files[i], "[^/\\]+$") or "?", "[^;]+") or "?") + end end - else - for i = #files, 1, -1 do - -- only store file name .. strip attributes from end and parent from beginning - table.insert(dirs, "\t" .. string.match(string.match(files[i], "[^/\\]+$") or "?", "[^;]+") or "?") + else -- get_attr == true + if not descending then + for i = 1, #files do + -- only store file name .. strip attributes from end and parent from beginning + table.insert(dirs, "\t" .. string.match(string.match(files[i], "[^/\\]+$") or "?", "[^;]+") or "?") + table.insert(attrs, string.match(files[i], ";(.+)")) + end + else + for i = #files, 1, -1 do + -- only store file name .. strip attributes from end and parent from beginning + table.insert(dirs, "\t" .. string.match(string.match(files[i], "[^/\\]+$") or "?", "[^;]+") or "?") + table.insert(attrs, string.match(files[i], ";(.+)")) + end end end for i=1, #files do files[i] = nil end -- empty table for reuse @@ -113,9 +132,9 @@ function file_choose(dir, title, sort_by, descending) dir = string.gsub(dirs[item], "%c+","") if not rb.dir_exists("/" .. dir) then if (parentdir == "/") then - return parentdir .. dir + return parentdir .. dir, attrs[item - ndirs - 1] -- -1 for hstr else - return parentdir .. "/" .. dir + return parentdir .. "/" .. dir, attrs[item - ndirs - 1] end end end @@ -125,7 +144,7 @@ function file_choose(dir, title, sort_by, descending) if dir == "" then dir = "/" end end for i=1, #dirs do dirs[i] = nil end -- empty table for reuse - + for i=1, #attrs do attrs[i] = nil end -- empty table for reuse end end -- file_choose -------------------------------------------------------------------------------- diff --git a/apps/plugins/lua/include_lua/get_files.lua b/apps/plugins/lua/include_lua/get_files.lua index 42c6c9c378..01a98d3ed7 100644 --- a/apps/plugins/lua/include_lua/get_files.lua +++ b/apps/plugins/lua/include_lua/get_files.lua @@ -27,7 +27,7 @@ if ... == nil then rb.splash(rb.HZ * 3, "use 'require'") end -- findfile & finddir are definable search functions -- if not defined all files/dirs are returned if false is passed.. none or you can provide your own function see below.. --- sort_by can be by "name" "size" "date" or "none" to perform no sorting +-- sort_by can be by "name" "size" "date" "type" or "none" to perform no sorting note: for "size" and "date" you may need to strip the attribute data to use the returned filename e.g. string.match(file, "[^;]+") -- cancel_fn if not defined or not a function no user cancel otherwise supply @@ -137,6 +137,23 @@ local function get_files(path, recurse, finddir, findfile, sort_by, cancel_fn, f end return s1 < s2 end + elseif sort_by == "type" then + filepath_function = function(path, sep, fname, fattrib, fsize, ftime) + return string.format("%s%s%s; At:%d, Sz:%d, Tm:%d", path, sep, fname, fattrib, fsize, ftime) + end + sort_by_function = function(s1, s2) + local v1, v2 + v1 = string.match(s1, ".+%.([^%s]+).AT:") + v2 = string.match(s2, ".+%.([^%s]+).AT:") + + if v1 or v2 then + --rb.splash(20, (v1 or "?") .. " " .. (v2 or "?")) + if (v1 ~= v2) then + return (v1 or "") < (v2 or "") + end + end + return s1 < s2 + end else -- "none" filepath_function = function(path, sep, fname, fattrib, fsize, ftime) return string.format("%s%s%s", path, sep, fname) diff --git a/apps/plugins/lua_scripts/file_browser.lua b/apps/plugins/lua_scripts/file_browser.lua index 8a16a1a544..0bcaed9101 100644 --- a/apps/plugins/lua_scripts/file_browser.lua +++ b/apps/plugins/lua_scripts/file_browser.lua @@ -34,6 +34,7 @@ package.path = scrpath .. "/?.lua;" .. package.path --add lua_scripts directory require("printmenus") --menu require("filebrowse") -- file browser +require("file_attrs") rb.actions = nil package.loaded["actions"] = nil @@ -45,22 +46,61 @@ function main_menu() [2] = "Sort by Name", [3] = "Sort by Size", [4] = "Sort by Date", - [5] = "Exit" + [5] = "Sort by Type", + [6] = "Exit" } local ft = { [0] = exit_now, --if user cancels do this function [1] = function(TITLE) return true end, -- shouldn't happen title occupies this slot [2] = function(SBNAME) - _lcd:splashf(rb.HZ, "%s", file_choose("/", "", "name", false) or "None") + _lcd:splashf(rb.HZ, "%s", file_choose("/", "", "name") or "None") end, [3] = function(SBSIZE) - _lcd:splashf(rb.HZ, "%s", file_choose("/", "", "size", true) or "None") + local file, attr = file_choose("/", "", "size", true, true) + if file and attr then + local sz = tonumber(string.match(attr, ".+Sz:(%d+)") or 0) + if sz > 1024 then + _lcd:splashf(rb.HZ, "%s", file .. " " .. sz / 1024 .. " kiB") + else + _lcd:splashf(rb.HZ, "%s", file .. " " .. sz .. " B") + end + else + _lcd:splashf(rb.HZ, "%s", file or "None") + end + --_lcd:splashf(rb.HZ, "%s", file_choose("/", "", "size") or "None") end, [4] = function(SBDATE) - _lcd:splashf(rb.HZ, "%s", file_choose("/", "", "date") or "None") + local file, attr = file_choose("/", "", "date", true, true) + if file and attr then + local tm = tonumber(string.match(attr, ".+Tm:(%d+)") or 0) + _lcd:splashf(rb.HZ, "%s", file .. " " .. attr)--" Tm:" .. tm) + else + _lcd:splashf(rb.HZ, "%s", file or "None") + end + --_lcd:splashf(rb.HZ, "%s", file_choose("/", "", "date") or "None") end, - [5] = function(EXIT_) return true end + [5] = function(SBTYPE) + local file = file_choose("/", "", "type") + if file then + local known = false + local attr = rb.filetype_get_attr(file) + for k, v in pairs(rb) do + if nil ~= string.find (k, "^FILE_ATTR_(.+)") then + if v == attr then + file = file .. " " .. k + known = true + break + end + end + end + if not known then + file = file .. " Unknown Type " .. string.match(file, "%.[^%.]+$") or "" + end + end + _lcd:splashf(rb.HZ, "%s", file or "None") + end, + [6] = function(EXIT_) return true end } print_menu(mt, ft) From 4f65dfa64917f92d2c215b3cde50ca66ee9e9daf Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 30 Jul 2026 08:12:55 +0300 Subject: [PATCH 059/408] hiby: r1_patcher: add separate pack/unpack/inject-app options Change-Id: I87f513b55b1cb58b6c1c3f307fcb734e36bda8e3 --- tools/r1_patcher/r1_patcher.sh | 250 ++++++++++++++++++++++----------- 1 file changed, 171 insertions(+), 79 deletions(-) diff --git a/tools/r1_patcher/r1_patcher.sh b/tools/r1_patcher/r1_patcher.sh index 7cc102044b..def1112565 100755 --- a/tools/r1_patcher/r1_patcher.sh +++ b/tools/r1_patcher/r1_patcher.sh @@ -3,66 +3,143 @@ #Prerequisites: #sudo apt update && sudo apt install -y p7zip-full squashfs-tools genisoimage coreutils -if [[ $# -ne 2 ]]; then - echo 'usage: ./r1_patcher.sh r1.upt bootloader.r1' >&2 - exit 1 -fi - # Fail fast on any error, undefined variable, or failed pipeline set -euo pipefail # Report errors with line number and the failing command trap 'echo "ERROR at line ${LINENO}: ${BASH_COMMAND}" >&2' ERR +usage() { + echo 'Usage:' >&2 + echo ' ./r1_patcher.sh r1.upt bootloader.r1' >&2 + echo '' >&2 + echo 'Advanced usage:' >&2 + echo ' ./r1_patcher.sh --unpack r1.upt [working_dir]' >&2 + echo ' ./r1_patcher.sh --inject-app bootloader.r1 [working_dir]' >&2 + echo ' ./r1_patcher.sh --pack [working_dir] [output.upt]' >&2 + exit 1 +} + +mode="" +if [[ $# -ge 1 && "$1" == --* ]]; then + mode="$1" + shift +fi + +case "$mode" in + "") + if [[ $# -ne 2 ]]; then + usage + fi + updatefile="$1" + bootloader="$2" + workingdir="$(realpath -m ./working_dir)" + updatefile_rb="${updatefile%.*}_rb.upt" + do_unpack=1 + do_inject=1 + do_pack=1 + do_cleanup=1 + ;; + --unpack) + if [[ $# -lt 1 || $# -gt 2 ]]; then + usage + fi + updatefile="$1" + workingdir="${2:-./working_dir}" + workingdir="$(realpath -m "$workingdir")" + do_unpack=1 + do_inject=0 + do_pack=0 + do_cleanup=0 + ;; + --inject-app) + if [[ $# -lt 1 || $# -gt 2 ]]; then + usage + fi + bootloader="$1" + workingdir="${2:-./working_dir}" + workingdir="$(realpath -m "$workingdir")" + do_unpack=0 + do_inject=1 + do_pack=0 + do_cleanup=0 + ;; + --pack) + if [[ $# -lt 1 || $# -gt 2 ]]; then + usage + fi + workingdir="${1:-./working_dir}" + workingdir="$(realpath -m "$workingdir")" + if [[ $# -ge 2 ]]; then + updatefile_rb="$2" + else + updatefile_rb="$(basename "$workingdir")_rb.upt" + fi + do_unpack=0 + do_inject=0 + do_pack=1 + do_cleanup=0 + ;; + *) + usage + ;; +esac + +currentdir="$(pwd)" + +if [[ -n "${updatefile_rb:-}" && "$updatefile_rb" != /* ]]; then + updatefile_rb="$currentdir/$updatefile_rb" +fi + ################################################################################ ### init ################################################################################ -currentdir="$(pwd)" -updatefile="$(basename "$1")" -updatefile_rb="${updatefile%.*}_rb.upt" - -workingdir="$(realpath ./working_dir)" workingdir_in="$workingdir/in" workingdir_out="$workingdir/out" -rm -rf "$workingdir" +if [[ "$do_unpack" -eq 1 ]]; then + rm -rf "$workingdir" -mkdir "$workingdir" -mkdir "$workingdir_in" -mkdir "$workingdir_out" + mkdir "$workingdir" + mkdir "$workingdir_in" + mkdir "$workingdir_out" +fi ################################################################################ ### extract ################################################################################ -# extract iso -mkdir -p "$workingdir_in/image_contents" -7z -o"$workingdir_in/image_contents" x "$updatefile" +if [[ "$do_unpack" -eq 1 ]]; then + # extract iso + mkdir -p "$workingdir_in/image_contents" + 7z -o"$workingdir_in/image_contents" x "$updatefile" -# create xImage (unchanged) -mkdir -p "$workingdir_out/xImage" -cat "$workingdir_in/image_contents/ota_v0"/xImage.* > "$workingdir_out/xImage/xImage" + # create xImage (unchanged) + mkdir -p "$workingdir_out/xImage" + cat "$workingdir_in/image_contents/ota_v0"/xImage.* > "$workingdir_out/xImage/xImage" -# create rootfs -mkdir -p "$workingdir_in/rootfs" -mkdir -p "$workingdir_in/rootfs/extracted" -cat "$workingdir_in/image_contents/ota_v0"/rootfs.squashfs.* > "$workingdir_in/rootfs/rootfs.squashfs" + # create rootfs + mkdir -p "$workingdir_in/rootfs" + mkdir -p "$workingdir_in/rootfs/extracted" + cat "$workingdir_in/image_contents/ota_v0"/rootfs.squashfs.* > "$workingdir_in/rootfs/rootfs.squashfs" -# extract rootfs -unsquashfs -f -d "$workingdir_in/rootfs/extracted" "$workingdir_in/rootfs/rootfs.squashfs" + # extract rootfs + unsquashfs -f -d "$workingdir_in/rootfs/extracted" "$workingdir_in/rootfs/rootfs.squashfs" +fi ################################################################################ ### update ################################################################################ -# copy 'bootloader' -bootloader_file="bootloader.rb" -cp "$2" "$workingdir_in/rootfs/extracted/usr/bin/$bootloader_file" -chmod 0755 "$workingdir_in/rootfs/extracted/usr/bin/$bootloader_file" +if [[ "$do_inject" -eq 1 ]]; then + # copy 'bootloader' + bootloader_file="bootloader.rb" + cp "$bootloader" "$workingdir_in/rootfs/extracted/usr/bin/$bootloader_file" + chmod 0755 "$workingdir_in/rootfs/extracted/usr/bin/$bootloader_file" -# create modified 'hiby_player.sh' script -cat << EOF > "$workingdir_in/rootfs/extracted/usr/bin/hiby_player.sh" + # create modified 'hiby_player.sh' script + cat << EOF > "$workingdir_in/rootfs/extracted/usr/bin/hiby_player.sh" #!/bin/sh killall hiby_player &>/dev/null @@ -74,14 +151,16 @@ killall -9 $bootloader_file &>/dev/null /usr/bin/$bootloader_file sleep 1s EOF -chmod 0755 "$workingdir_in/rootfs/extracted/usr/bin/hiby_player.sh" + chmod 0755 "$workingdir_in/rootfs/extracted/usr/bin/hiby_player.sh" +fi ################################################################################ ### Rockbox Hotplug Logic ################################################################################ -# 1. Create hotplug helper script -cat << 'EOF' > "$workingdir_in/rootfs/extracted/usr/bin/rb_hotplug.sh" +if [[ "$do_inject" -eq 1 ]]; then + # 1. Create hotplug helper script + cat << 'EOF' > "$workingdir_in/rootfs/extracted/usr/bin/rb_hotplug.sh" #!/bin/sh MNT_SD="/data/mnt/sd_0" MNT_USB="/data/mnt/usb" @@ -102,64 +181,67 @@ case "$ACTION" in ;; esac EOF -chmod 0755 "$workingdir_in/rootfs/extracted/usr/bin/rb_hotplug.sh" + chmod 0755 "$workingdir_in/rootfs/extracted/usr/bin/rb_hotplug.sh" -# 2. Check mdev.conf and append rules only if missing -MDEV_CONF="$workingdir_in/rootfs/extracted/etc/mdev.conf" + # 2. Check mdev.conf and append rules only if missing + MDEV_CONF="$workingdir_in/rootfs/extracted/etc/mdev.conf" -if ! grep -q "mmcblk" "$MDEV_CONF"; then - echo "mmcblk[0-9]p[0-9] 0:0 660 */usr/bin/rb_hotplug.sh" >> "$MDEV_CONF" -fi + if ! grep -q "mmcblk" "$MDEV_CONF"; then + echo "mmcblk[0-9]p[0-9] 0:0 660 */usr/bin/rb_hotplug.sh" >> "$MDEV_CONF" + fi -if ! grep -q "sd\[a-z\]" "$MDEV_CONF"; then - echo "sd[a-z][0-9] 0:0 660 */usr/bin/rb_hotplug.sh" >> "$MDEV_CONF" + if ! grep -q "sd\[a-z\]" "$MDEV_CONF"; then + echo "sd[a-z][0-9] 0:0 660 */usr/bin/rb_hotplug.sh" >> "$MDEV_CONF" + fi fi ################################################################################ ### rebuild ################################################################################ -mkdir -p "$workingdir_out/image_contents/ota_v0" +if [[ "$do_pack" -eq 1 ]]; then + mkdir -p "$workingdir_out/image_contents/ota_v0" -mksquashfs "$workingdir_in/rootfs/extracted" "$workingdir_out/rootfs.squashfs" -comp lzo -all-root + rm -f "$workingdir_out/rootfs.squashfs" + mksquashfs "$workingdir_in/rootfs/extracted" "$workingdir_out/rootfs.squashfs" -comp lzo -all-root -noappend -cd "$workingdir_out/image_contents/ota_v0" + cd "$workingdir_out/image_contents/ota_v0" -# rootfs.squashfs -split -b 512k "$workingdir_out/rootfs.squashfs" --numeric-suffixes=0 -a 4 rootfs.squashfs. + # rootfs.squashfs + split -b 512k "$workingdir_out/rootfs.squashfs" --numeric-suffixes=0 -a 4 rootfs.squashfs. -rootfs_md5=($(md5sum "$workingdir_out/rootfs.squashfs")) -rootfs_size=$(stat -c%s "$workingdir_out/rootfs.squashfs") -md5=$rootfs_md5 + rootfs_md5=($(md5sum "$workingdir_out/rootfs.squashfs")) + rootfs_size=$(stat -c%s "$workingdir_out/rootfs.squashfs") + md5=$rootfs_md5 -ota_md5_rootfs="ota_md5_rootfs.squashfs.$md5" + ota_md5_rootfs="ota_md5_rootfs.squashfs.$md5" -for part in $(ls rootfs.squashfs.[0-9]* | sort); do - md5next=($(md5sum "$part")) - echo $md5next >> $ota_md5_rootfs - mv "$part" "$part.$md5" - md5=$md5next -done + for part in $(ls rootfs.squashfs.[0-9]* | sort); do + md5next=($(md5sum "$part")) + echo $md5next >> $ota_md5_rootfs + mv "$part" "$part.$md5" + md5=$md5next + done -# xImage -split -b 512k "$workingdir_out/xImage/xImage" --numeric-suffixes=0 -a 4 xImage. + # xImage + split -b 512k "$workingdir_out/xImage/xImage" --numeric-suffixes=0 -a 4 xImage. -ximage_md5=($(md5sum "$workingdir_out/xImage/xImage")) -ximage_size=$(stat -c%s "$workingdir_out/xImage/xImage") -md5=$ximage_md5 + ximage_md5=($(md5sum "$workingdir_out/xImage/xImage")) + ximage_size=$(stat -c%s "$workingdir_out/xImage/xImage") + md5=$ximage_md5 -ota_md5_xImage="ota_md5_xImage.$md5" + ota_md5_xImage="ota_md5_xImage.$md5" -for part in $(ls xImage.[0-9]* | sort); do - md5next=($(md5sum "$part")) - echo $md5next >> $ota_md5_xImage - mv "$part" "$part.$md5" - md5=$md5next -done + for part in $(ls xImage.[0-9]* | sort); do + md5next=($(md5sum "$part")) + echo $md5next >> $ota_md5_xImage + mv "$part" "$part.$md5" + md5=$md5next + done -# ota_update.in + # ota_update.in -echo "ota_version=0 + echo "ota_version=0 img_type=kernel img_name=xImage @@ -172,20 +254,30 @@ img_size=$rootfs_size img_md5=$rootfs_md5 " > ota_update.in -echo > ota_v0.ok + echo > ota_v0.ok -# ota_config.in -cd "$workingdir_out/image_contents" + # ota_config.in + cd "$workingdir_out/image_contents" -echo "current_version=0" > ota_config.in + echo "current_version=0" > ota_config.in -# iso -genisoimage -f -U -J -joliet-long -r -allow-lowercase -allow-multidot -o "$currentdir/$updatefile_rb" "$workingdir_out/image_contents/" + # iso + genisoimage -f -U -J -joliet-long -r -allow-lowercase -allow-multidot -input-charset utf-8 -o "$updatefile_rb" "$workingdir_out/image_contents/" +fi ################################################################################ ### cleanup ################################################################################ -rm -rf "$workingdir" +if [[ "$do_cleanup" -eq 1 ]]; then + rm -rf "$workingdir" +else + if [[ "$do_unpack" -eq 1 ]]; then + echo "Extracted to: $workingdir/in/rootfs/extracted" + fi +fi +if [[ "$do_pack" -eq 1 ]]; then + echo "Image file created: $updatefile_rb" +fi exit 0 From eecd4ec98b06c5bd957aaf8120f7d5514042db29 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Wed, 5 Aug 2026 22:29:02 +0300 Subject: [PATCH 060/408] hiby: r1_patcher: fix sd hotplug on newer firmwares flush mount option is no longer available. Change-Id: I2380c1adcd2c2ba914db492eff1ed49bb45cb30c --- tools/r1_patcher/r1_patcher.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/r1_patcher/r1_patcher.sh b/tools/r1_patcher/r1_patcher.sh index def1112565..b82a80457d 100755 --- a/tools/r1_patcher/r1_patcher.sh +++ b/tools/r1_patcher/r1_patcher.sh @@ -174,7 +174,7 @@ esac case "$ACTION" in add|"") mkdir -p "$MNT" - mount -t auto -o flush,noatime "/dev/$MDEV" "$MNT" + mount -t auto -o sync,noatime "/dev/$MDEV" "$MNT" ;; remove) umount -l "$MNT" From 21e95258f2d3f7c8c62cd01872afe9926301bf8d Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 9 Aug 2026 08:00:50 -0400 Subject: [PATCH 061/408] FS#13977 - rbutil: fix SAPI5 TTS test output and playback (Alessio Lenzi) After the SAPI5 error handling added for FS#13972, the Test TTS button can fail with SAPI error 5 even though the engine and voice are configured correctly. The test also destroys its QSoundEffect and temporary file before asynchronous playback can complete. Config::testTts() used a QTemporaryFile with no .wav extension. TTSSapi::voice() removes the requested output before synthesis so that a stale file cannot be mistaken for successful output. SAPI SpFileStream was therefore asked to create an extensionless output file and returned error 5 (invalid procedure call or argument). After successful synthesis, QSoundEffect was allocated on the stack, its loop count was set to zero, and the temporary file was removed when testTts() returned. This does not allow asynchronous playback to complete reliably. * create a temporary directory and requests an initially nonexistent tts-test.wav inside it; * keep the directory and generated wave file alive for playback; * keep QSoundEffect alive until playback finishes or fails; * request one playback and clean up all temporary data afterward. Change-Id: I8e482ab846e6445889118e121025ec48d3776d6b --- utils/rbutilqt/configure.cpp | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/utils/rbutilqt/configure.cpp b/utils/rbutilqt/configure.cpp index 7b6d634c3f..d8a2a5a4c2 100644 --- a/utils/rbutilqt/configure.cpp +++ b/utils/rbutilqt/configure.cpp @@ -939,17 +939,20 @@ void Config::testTts() } QString filename; - QTemporaryFile file(this); + QTemporaryDir* tempDir = nullptr; // keep filename empty if the TTS can do speaking for itself. if(!(tts->capabilities() & TTSBase::CanSpeak)) { - file.open(); - filename = file.fileName(); - file.close(); + // SAPI's SpFileStream requires a filename with a wave extension. + // Give the engine a path that does not exist yet, matching normal + // voice-file generation, and keep its directory until playback ends. + tempDir = new QTemporaryDir(); + filename = tempDir->filePath("tts-test.wav"); } if(tts->voice(tr("Rockbox Utility Voice Test"),filename,&errstr) == FatalError) { tts->stop(); + delete tempDir; QMessageBox::warning(this,tr("Could not voice test string."), tr("Could not voice test string.\n") + errstr + tr("\nPlease configure TTS engine.")); @@ -957,17 +960,28 @@ void Config::testTts() return; } tts->stop(); - if(!filename.isEmpty()) { - QSoundEffect effect; - effect.setSource(QUrl::fromLocalFile(filename)); - effect.setLoopCount(0); - effect.setVolume(1.0f); - effect.play(); - } ui.testTTS->setEnabled(true); delete tts; /* Config objects are never deleted (in fact, they are leaked..), so we can't rely on QObject, since that would delete the TTSBase instance on application exit */ + if(tempDir != nullptr) { + QSoundEffect* effect = new QSoundEffect(this); + connect(effect, &QObject::destroyed, [tempDir]() { + delete tempDir; + }); + connect(effect, &QSoundEffect::playingChanged, effect, [effect]() { + if(!effect->isPlaying()) + effect->deleteLater(); + }); + connect(effect, &QSoundEffect::statusChanged, effect, [effect]() { + if(effect->status() == QSoundEffect::Error) + effect->deleteLater(); + }); + effect->setSource(QUrl::fromLocalFile(filename)); + effect->setLoopCount(1); + effect->setVolume(1.0f); + effect->play(); + } #endif } From 85c1ff8667f276f586f98de9546f6c3d5bec2049 Mon Sep 17 00:00:00 2001 From: Skye Date: Fri, 7 Aug 2026 22:27:45 +0900 Subject: [PATCH 062/408] change build scripts to make reproducible builds possible - makes SOURCE_DATE_EPOCH affect BUILDDATE and version string - makes wpsbuild.pl output deterministic - omits unnecessary extended metadata from zip files - runs strip-nondeterminism on zip files if REPRODUCIBLE_ZIP is set Change-Id: I1617e3f5233b2bc356fcfa6457be1a7785c303f9 --- apps/lang/lang.make | 5 ++++- tools/buildzip.pl | 2 +- tools/configure | 4 ++++ tools/root.make | 3 +++ tools/version.sh | 6 +++++- wps/wpsbuild.pl | 2 +- 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/lang/lang.make b/apps/lang/lang.make index 2952ac5aef..5923597e86 100644 --- a/apps/lang/lang.make +++ b/apps/lang/lang.make @@ -62,7 +62,10 @@ $(BUILDDIR)/apps/lang/lang-enum.txt: $(BUILDDIR)/lang_enum.h $(BUILDDIR)/apps/lang/voicestrings.zip: $(VOICEOBJ) $(wildcard $(BUILDDIR)/apps/lang/*.talk) $(BUILDDIR)/apps/lang/voice-corrections.txt $(BUILDDIR)/apps/lang/lang-enum.txt $(call PRINTS,ZIP $(subst $(BUILDDIR)/,,$@)) - $(SILENT)zip -9 -q $@ $(subst $(BUILDDIR)/,,$^) + $(SILENT)zip -X -9 -q $@ $(subst $(BUILDDIR)/,,$^) +ifneq ($(REPRODUCIBLE_ZIP),) + $(SILENT)strip-nondeterminism $@ +endif #copy any included talk files to the /lang directory $(BUILDDIR)/apps/lang/%.talk: $(ROOTDIR)/apps/lang/%.talk diff --git a/tools/buildzip.pl b/tools/buildzip.pl index 0613be1484..8f546db270 100755 --- a/tools/buildzip.pl +++ b/tools/buildzip.pl @@ -259,7 +259,7 @@ GetOptions ( 'r|root=s' => \$ROOT, # Thus we use the ":" for those for which we use a default value in case of "" # and assign the default value afterwards if ($ziptool eq '') { - $ziptool = "zip -r9"; + $ziptool = "zip -r9 -X"; } if ($output eq '') { $output = "rockbox.zip" diff --git a/tools/configure b/tools/configure index ff19ab75ab..608f783763 100755 --- a/tools/configure +++ b/tools/configure @@ -4915,7 +4915,11 @@ export BUILDDIR=${pwd} export RBCODEC_BLD=${pwd}/lib/rbcodec export VOICELANGUAGE=${voicelanguage} export MEMORYSIZE=${memory} +ifeq (\$(SOURCE_DATE_EPOCH),) export BUILDDATE:=\$(shell date -u +'-DYEAR=%Y -DMONTH=%m -DDAY=%d') +else +export BUILDDATE:=\$(shell date -d "@\$(SOURCE_DATE_EPOCH)" -u +'-DYEAR=%Y -DMONTH=%m -DDAY=%d') +endif export MKFIRMWARE=${tool} export BMP2RB_MONO=${bmp2rb_mono} export BMP2RB_NATIVE=${bmp2rb_native} diff --git a/tools/root.make b/tools/root.make index 11540a0a39..46acf85e78 100644 --- a/tools/root.make +++ b/tools/root.make @@ -351,6 +351,9 @@ $(BUILDDIR)/rockbox.zip: build endif $(call PRINTS,ZIP rockbox$(ZIPFILESUFFIX).zip) $(SILENT)$(TOOLSDIR)/buildzip.pl $(VERBOSEOPT) --app=$(APPLICATION) -m \"$(MODELNAME)\" -i \"$(TARGET_ID)\" -o $(BUILDDIR)/rockbox$(ZIPFILESUFFIX).zip -r "$(ROOTDIR)" --rbdir="$(RBDIR)" $(BUILDZIPOPTS) $(TARGET) $(BINARY) +ifneq ($(REPRODUCIBLE_ZIP),) + $(SILENT)strip-nondeterminism $(BUILDDIR)/rockbox$(ZIPFILESUFFIX).zip +endif mapzip: $(SILENT)find . -name "*.map" | xargs zip rockbox-maps.zip diff --git a/tools/version.sh b/tools/version.sh index 2fcdcd02c4..f2b2196ee5 100755 --- a/tools/version.sh +++ b/tools/version.sh @@ -50,7 +50,11 @@ if [ -z $VERSION ]; then # Ok, we need to derive it from the Version Control system VER=`gitversion $TOP` fi -VERSION=$VER-`date -u +%y%m%d` +if [ -z $SOURCE_DATE_EPOCH ]; then + VERSION=$VER-`date -u +%y%m%d` +else + VERSION=$VER-`date -d @$SOURCE_DATE_EPOCH -u +%y%m%d` +fi fi echo $VERSION diff --git a/wps/wpsbuild.pl b/wps/wpsbuild.pl index e3e8c178ec..ccc4cad1f4 100755 --- a/wps/wpsbuild.pl +++ b/wps/wpsbuild.pl @@ -268,7 +268,7 @@ MOO "rwps" => $rwps, "rsbs" => $rsbs, "rfms" => $rfms); - for my $skin (keys %skinfiles) { + for my $skin (sort keys %skinfiles) { my $val = $skinfiles{$skin}; if (!defined($val)) { # dont put value if not defined (e.g. rwps when there's no remote) From 0726ec93517a61f602679ab052b083217ec9c96d Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 9 Aug 2026 15:54:58 -0400 Subject: [PATCH 063/408] FS#13978 - rbutil: Improve GUI responsiveness during voice/talk file generation (Alessio Lenzi) Voice file and Talk clip generation currently run synchronously in the GUI thread. During long TTS operations this prevents the progress window from processing input and accessibility events. On Windows this makes the progress list unavailable to screen readers such as NVDA and can make Rockbox Utility appear to be hung. The attached patch moves VoiceFileCreator and TalkFileCreator to low-priority QThreads while keeping ProgressLoggerGui in the GUI thread. Progress and log signals are delivered through queued Qt connections. Cancellation remains available while a synchronous TTS request is running. The abort flags used across threads are atomic and the abort signal is sent through a direct connection. Worker objects and threads are deleted through the standard finished / deleteLater lifecycle. Talk generation now passes all selected folders to one worker and processes them sequentially, emitting the final done signal only once. This preserves multiple-folder selection without starting work in the GUI thread. Change-Id: I58aa66333d43e1be249c84e0cf46c17c119a8360 --- utils/rbutilqt/base/talkfile.cpp | 34 ++++++++++++++++++++++++---- utils/rbutilqt/base/talkfile.h | 6 ++++- utils/rbutilqt/base/talkgenerator.h | 3 ++- utils/rbutilqt/base/voicefile.cpp | 6 ++--- utils/rbutilqt/base/voicefile.h | 1 - utils/rbutilqt/createvoicewindow.cpp | 32 +++++++++++++++++++++++--- utils/rbutilqt/createvoicewindow.h | 5 +++- utils/rbutilqt/installtalkwindow.cpp | 31 ++++++++++++++++++------- utils/rbutilqt/installtalkwindow.h | 5 +++- 9 files changed, 99 insertions(+), 24 deletions(-) diff --git a/utils/rbutilqt/base/talkfile.cpp b/utils/rbutilqt/base/talkfile.cpp index 50afb1f323..3cd265b7f1 100644 --- a/utils/rbutilqt/base/talkfile.cpp +++ b/utils/rbutilqt/base/talkfile.cpp @@ -31,6 +31,34 @@ TalkFileCreator::TalkFileCreator(QObject* parent): QObject(parent) bool TalkFileCreator::createTalkFiles() { m_abort = false; + QStringList dirs = m_dirs; + if(dirs.isEmpty()) + dirs.append(m_dir); + + for(const QString& dir : dirs) { + if(m_abort) + break; + + m_dir = dir; + LOG_INFO() << "creating talk files for folder" << m_dir; + if(!createTalkFilesForDir()) { + emit done(true); + return false; + } + } + + if(m_abort) { + doAbort(); + emit done(true); + return false; + } + + emit done(false); + return true; +} + +bool TalkFileCreator::createTalkFilesForDir() +{ QString errStr; emit logItem(tr("Starting Talk file generation for folder %1") @@ -51,10 +79,10 @@ bool TalkFileCreator::createTalkFiles() // generate entries TalkGenerator generator(this); // no string corrections yet: do not set language for TalkGenerator. - connect(&generator, &TalkGenerator::done, this, &TalkFileCreator::done); connect(&generator, &TalkGenerator::logItem, this, &TalkFileCreator::logItem); connect(&generator, &TalkGenerator::logProgress, this, &TalkFileCreator::logProgress); - connect(this, &TalkFileCreator::aborted, &generator, &TalkGenerator::abort); + connect(this, &TalkFileCreator::aborted, + &generator, &TalkGenerator::abort, Qt::DirectConnection); if(generator.process(&m_talkList) == TalkGenerator::eERROR) { @@ -77,7 +105,6 @@ bool TalkFileCreator::createTalkFiles() emit logItem(tr("Finished creating Talk files"),LOGOK); emit logProgress(1,1); - emit done(false); return true; } @@ -101,7 +128,6 @@ void TalkFileCreator::doAbort() { cleanup(); emit logProgress(0,1); - emit done(true); } //! \brief creates a list of what to generate //! diff --git a/utils/rbutilqt/base/talkfile.h b/utils/rbutilqt/base/talkfile.h index 014753da2a..3b57964aaf 100644 --- a/utils/rbutilqt/base/talkfile.h +++ b/utils/rbutilqt/base/talkfile.h @@ -22,6 +22,7 @@ #ifndef TALKFILE_H #define TALKFILE_H +#include #include #include "progressloglevels.h" @@ -37,6 +38,7 @@ public: bool createTalkFiles(); void setDir(QString dir) {m_dir = dir;} + void setDirs(QStringList dirs) {m_dirs = dirs;} void setMountPoint(QString mountpoint) {m_mountpoint = mountpoint;} void setGenerateOnlyNew(bool ov) {m_generateOnlyNew = ov;} @@ -55,6 +57,7 @@ signals: void logProgress(int, int); //! set progress bar. private: + bool createTalkFilesForDir(); bool cleanup(); QString stripExtension(QString filename); void doAbort(); @@ -64,6 +67,7 @@ private: bool createTalkList(QDir startDir); QString m_dir; + QStringList m_dirs; QString m_mountpoint; bool m_generateOnlyNew; @@ -73,7 +77,7 @@ private: bool m_talkFiles; QStringList m_ignoreFiles; - bool m_abort; + std::atomic_bool m_abort{false}; QList m_talkList; }; diff --git a/utils/rbutilqt/base/talkgenerator.h b/utils/rbutilqt/base/talkgenerator.h index 6bdb22745e..95da967d4d 100644 --- a/utils/rbutilqt/base/talkgenerator.h +++ b/utils/rbutilqt/base/talkgenerator.h @@ -22,6 +22,7 @@ #ifndef TALKGENERATOR_H #define TALKGENERATOR_H +#include #include #include "progressloglevels.h" @@ -81,7 +82,7 @@ private: }; QList m_corrections; - bool m_abort; + std::atomic_bool m_abort{false}; }; diff --git a/utils/rbutilqt/base/voicefile.cpp b/utils/rbutilqt/base/voicefile.cpp index aaafd64cc8..32845edcec 100644 --- a/utils/rbutilqt/base/voicefile.cpp +++ b/utils/rbutilqt/base/voicefile.cpp @@ -33,15 +33,12 @@ VoiceFileCreator::VoiceFileCreator(QObject* parent) :QObject(parent) void VoiceFileCreator::abort() { - m_abort = true; emit aborted(); } bool VoiceFileCreator::createVoiceFile() { m_talkList.clear(); - m_abort = false; - delete corrFile; corrFile = new QFile(":/builtin/voice-corrections.txt", this); corrFile->open(QIODevice::ReadOnly); @@ -335,7 +332,8 @@ void VoiceFileCreator::create(void) connect(&generator, &TalkGenerator::done, this, &VoiceFileCreator::done); connect(&generator, &TalkGenerator::logItem, this, &VoiceFileCreator::logItem); connect(&generator, &TalkGenerator::logProgress, this, &VoiceFileCreator::logProgress); - connect(this, &VoiceFileCreator::aborted, &generator, &TalkGenerator::abort); + connect(this, &VoiceFileCreator::aborted, + &generator, &TalkGenerator::abort, Qt::DirectConnection); if(generator.process(&m_talkList, m_wavtrimThreshold) == TalkGenerator::eERROR) { diff --git a/utils/rbutilqt/base/voicefile.h b/utils/rbutilqt/base/voicefile.h index 2c63ae4c80..35abe2ec60 100644 --- a/utils/rbutilqt/base/voicefile.h +++ b/utils/rbutilqt/base/voicefile.h @@ -70,7 +70,6 @@ private: int m_voiceformat; QFile *corrFile; // the voice-corrections file - bool m_abort; QList m_talkList; }; diff --git a/utils/rbutilqt/createvoicewindow.cpp b/utils/rbutilqt/createvoicewindow.cpp index f4679a2418..2d44ab0d32 100644 --- a/utils/rbutilqt/createvoicewindow.cpp +++ b/utils/rbutilqt/createvoicewindow.cpp @@ -28,7 +28,6 @@ CreateVoiceWindow::CreateVoiceWindow(QWidget *parent) : QDialog(parent) { ui.setupUi(this); - voicecreator = new VoiceFileCreator(this); updateSettings(); connect(ui.change,&QAbstractButton::clicked,this,&CreateVoiceWindow::change); } @@ -46,12 +45,22 @@ void CreateVoiceWindow::change() void CreateVoiceWindow::accept() { + // Do not start a second job while voice creation is still running. + if(workerThread) + return; + logger = new ProgressLoggerGui(this); connect(logger,&ProgressLoggerGui::closed,this,&QWidget::close); logger->show(); saveSettings(); + // Voice generation invokes the TTS engine and encoder synchronously. Run + // it in a worker thread so the GUI (including accessibility support) stays + // responsive while a voice file is being created. + workerThread = new QThread(this); + voicecreator = new VoiceFileCreator(nullptr); + //configure voicecreator QString suffix = RbSettings::value(RbSettings::Suffix).toString(); QString mountpoint = RbSettings::value(RbSettings::Mountpoint).toString(); @@ -70,8 +79,25 @@ void CreateVoiceWindow::accept() connect(voicecreator, &VoiceFileCreator::done, logger, &ProgressLoggerGui::setFinished); connect(voicecreator, &VoiceFileCreator::logItem, logger, &ProgressLoggerGui::addItem); connect(voicecreator, &VoiceFileCreator::logProgress, logger, &ProgressLoggerGui::setProgress); - connect(logger,&ProgressLoggerGui::aborted,voicecreator,&VoiceFileCreator::abort); - voicecreator->createVoiceFile(); + // abort() only emits a signal connected to the generator's atomic abort + // flag, so it is safe to call while the worker is inside a TTS request. + connect(logger, &ProgressLoggerGui::aborted, + voicecreator, &VoiceFileCreator::abort, Qt::DirectConnection); + + voicecreator->moveToThread(workerThread); + connect(workerThread, &QThread::started, + voicecreator, &VoiceFileCreator::createVoiceFile); + connect(voicecreator, &VoiceFileCreator::done, + workerThread, &QThread::quit); + connect(voicecreator, &VoiceFileCreator::done, + voicecreator, &QObject::deleteLater); + connect(workerThread, &QThread::finished, + workerThread, &QObject::deleteLater); + connect(workerThread, &QThread::finished, this, [this]() { + voicecreator = nullptr; + workerThread = nullptr; + }); + workerThread->start(QThread::LowPriority); } diff --git a/utils/rbutilqt/createvoicewindow.h b/utils/rbutilqt/createvoicewindow.h index 87127267c5..5e5c07caf6 100644 --- a/utils/rbutilqt/createvoicewindow.h +++ b/utils/rbutilqt/createvoicewindow.h @@ -22,6 +22,8 @@ #define CREATEVOICEWINDOW_H #include +#include +#include #include #include "ui_createvoicefrm.h" @@ -47,7 +49,8 @@ class CreateVoiceWindow : public QDialog private: void changeEvent(QEvent *event); - VoiceFileCreator* voicecreator; + QPointer voicecreator; + QPointer workerThread; Ui::CreateVoiceFrm ui; ProgressLoggerGui* logger; QUrl m_proxy; diff --git a/utils/rbutilqt/installtalkwindow.cpp b/utils/rbutilqt/installtalkwindow.cpp index c8bcdcec2e..d5cd8c76f3 100644 --- a/utils/rbutilqt/installtalkwindow.cpp +++ b/utils/rbutilqt/installtalkwindow.cpp @@ -27,7 +27,6 @@ InstallTalkWindow::InstallTalkWindow(QWidget *parent) : QDialog(parent) { ui.setupUi(this); - talkcreator = new TalkFileCreator(this); connect(ui.change,&QAbstractButton::clicked,this,&InstallTalkWindow::change); @@ -88,6 +87,9 @@ void InstallTalkWindow::change() void InstallTalkWindow::accept() { + if(workerThread) + return; + saveSettings(); QStringList foldersToTalk = RbSettings::value(RbSettings::TalkFolders).toStringList(); @@ -103,7 +105,11 @@ void InstallTalkWindow::accept() connect(logger,&ProgressLoggerGui::closed,this,&QWidget::close); logger->show(); + workerThread = new QThread(this); + talkcreator = new TalkFileCreator(nullptr); + talkcreator->setMountPoint(RbSettings::value(RbSettings::Mountpoint).toString()); + talkcreator->setDirs(foldersToTalk); talkcreator->setGenerateOnlyNew(ui.GenerateOnlyNew->isChecked()); talkcreator->setRecursive(ui.recursive->isChecked()); @@ -115,14 +121,23 @@ void InstallTalkWindow::accept() connect(talkcreator, &TalkFileCreator::done, logger, &ProgressLoggerGui::setFinished); connect(talkcreator, &TalkFileCreator::logItem, logger, &ProgressLoggerGui::addItem); connect(talkcreator, &TalkFileCreator::logProgress, logger, &ProgressLoggerGui::setProgress); - connect(logger,&ProgressLoggerGui::aborted,talkcreator,&TalkFileCreator::abort); + connect(logger, &ProgressLoggerGui::aborted, + talkcreator, &TalkFileCreator::abort, Qt::DirectConnection); - for(int i = 0; i < foldersToTalk.size(); i++) { - LOG_INFO() << "creating talk files for folder" - << foldersToTalk.at(i); - talkcreator->setDir(foldersToTalk.at(i)); - talkcreator->createTalkFiles(); - } + talkcreator->moveToThread(workerThread); + connect(workerThread, &QThread::started, + talkcreator, &TalkFileCreator::createTalkFiles); + connect(talkcreator, &TalkFileCreator::done, + workerThread, &QThread::quit); + connect(talkcreator, &TalkFileCreator::done, + talkcreator, &QObject::deleteLater); + connect(workerThread, &QThread::finished, + workerThread, &QObject::deleteLater); + connect(workerThread, &QThread::finished, this, [this]() { + talkcreator = nullptr; + workerThread = nullptr; + }); + workerThread->start(QThread::LowPriority); } diff --git a/utils/rbutilqt/installtalkwindow.h b/utils/rbutilqt/installtalkwindow.h index f965e4cb09..3b2c34f04d 100644 --- a/utils/rbutilqt/installtalkwindow.h +++ b/utils/rbutilqt/installtalkwindow.h @@ -23,6 +23,8 @@ #include #include +#include +#include #include "ui_installtalkfrm.h" #include "progressloggergui.h" @@ -47,7 +49,8 @@ class InstallTalkWindow : public QDialog private: void changeEvent(QEvent *event); - TalkFileCreator* talkcreator; + QPointer talkcreator; + QPointer workerThread; Ui::InstallTalkFrm ui; ProgressLoggerGui* logger; QFileSystemModel *fsm; From 612453da482c20a61303a36479287a7e19c2bc74 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Mon, 10 Aug 2026 11:05:44 -0400 Subject: [PATCH 064/408] FS#13979 - rbutil: fix Talk file exclusion options and initial focus (Alessio Lenzi) 1. The "Ignore files" checkbox was saved in the settings but not consulted when starting generation. Patterns from the text field were therefore applied even when the checkbox was unchecked. The pattern list is now passed to TalkFileCreator only when the option is enabled. 2. Ignore patterns were converted to regular expressions by replacing only '*' and '?'. Other regular-expression characters were left unescaped and matches were not properly bounded. The patch uses Qt's wildcardToRegularExpression() conversion instead. 3. The Talk generation dialog initially focused "Strip Extensions" because of widget creation order. It now explicitly focuses the folder tree, matching the task flow and making keyboard and screen-reader use more predictable. 4. The handling of talkclips.ignore is also corrected so that files below a marked directory are skipped recursively. Previously Rockbox Utility could generate talkclips.ignore.talk and clips for files below .rockbox even though .rockbox/talkclips.ignore was present. Change-Id: I1f9f14ed7fc057bc254f62148a7ef8ffb69425ed --- utils/rbutilqt/base/talkfile.cpp | 28 ++++++++++++++++++++-------- utils/rbutilqt/installtalkwindow.cpp | 5 ++++- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/utils/rbutilqt/base/talkfile.cpp b/utils/rbutilqt/base/talkfile.cpp index 3cd265b7f1..40ceb03158 100644 --- a/utils/rbutilqt/base/talkfile.cpp +++ b/utils/rbutilqt/base/talkfile.cpp @@ -137,6 +137,20 @@ bool TalkFileCreator::createTalkList(QDir startDir) LOG_INFO() << "generating list of files" << startDir; m_talkList.clear(); + const QString startPath = QDir::cleanPath(startDir.absolutePath()); + auto isInIgnoredDirectory = [&startPath](const QFileInfo& fileInfo) { + QDir dir(fileInfo.isDir() ? fileInfo.absoluteFilePath() + : fileInfo.absolutePath()); + while(true) { + if(QFileInfo::exists(dir.filePath("talkclips.ignore"))) + return true; + + if(QDir::cleanPath(dir.absolutePath()) == startPath || !dir.cdUp()) + break; + } + return false; + }; + // create Iterator QDirIterator::IteratorFlags flags = QDirIterator::NoIteratorFlags; if(m_recursive) @@ -160,6 +174,10 @@ bool TalkFileCreator::createTalkList(QDir startDir) QFileInfo fileInf = it.fileInfo(); + // A talkclips.ignore file excludes its directory and all descendants. + if(isInIgnoredDirectory(fileInf)) + continue; + // its a dir if(fileInf.isDir()) { @@ -168,12 +186,6 @@ bool TalkFileCreator::createTalkList(QDir startDir) // insert into List if(!dir.dirName().isEmpty() && m_talkFolders) { - // check if we should ignore it - if(QFileInfo::exists(dir.path() + "/talkclips.ignore")) - { - continue; - } - // check to see if it's already covered if(m_generateOnlyNew && QFileInfo::exists(dir.path() + "/_dirname.talk")) { @@ -209,8 +221,8 @@ bool TalkFileCreator::createTalkList(QDir startDir) bool match = false; for(int i=0; i < m_ignoreFiles.size();i++) { - QString pattern = m_ignoreFiles[i].trimmed() - .replace("?", ".").replace("*", ".*"); + QString pattern = QRegularExpression::wildcardToRegularExpression( + m_ignoreFiles[i].trimmed()); QRegularExpression rx(pattern); if(rx.match(fileInf.fileName()).hasMatch()) match = true; diff --git a/utils/rbutilqt/installtalkwindow.cpp b/utils/rbutilqt/installtalkwindow.cpp index d5cd8c76f3..be6c93b1fb 100644 --- a/utils/rbutilqt/installtalkwindow.cpp +++ b/utils/rbutilqt/installtalkwindow.cpp @@ -46,6 +46,7 @@ InstallTalkWindow::InstallTalkWindow(QWidget *parent) : QDialog(parent) ui.treeView->setHeaderHidden(true); updateSettings(); + ui.treeView->setFocus(Qt::OtherFocusReason); } @@ -116,7 +117,9 @@ void InstallTalkWindow::accept() talkcreator->setStripExtensions(ui.StripExtensions->isChecked()); talkcreator->setTalkFolders(ui.talkFolders->isChecked()); talkcreator->setTalkFiles(ui.talkFiles->isChecked()); - talkcreator->setIgnoreFiles(ui.ignoreFiles->text().split(",", Qt::SkipEmptyParts)); + talkcreator->setIgnoreFiles(ui.ignoreEnabled->isChecked() + ? ui.ignoreFiles->text().split(",", Qt::SkipEmptyParts) + : QStringList()); connect(talkcreator, &TalkFileCreator::done, logger, &ProgressLoggerGui::setFinished); connect(talkcreator, &TalkFileCreator::logItem, logger, &ProgressLoggerGui::addItem); From a13368d0d65ea61d1c4ef4f7a9006a04d6f8e761 Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Sun, 9 Aug 2026 23:21:02 +0100 Subject: [PATCH 065/408] axp2101: fix the enable test in axp2101_supply_get_voltage() The enable check read if(r & (1 << info->en_bit) == 0) `==` binds tighter than `&`, so this evaluates as `r & ((1 << bit) == 0)`, i.e. `r & 0`, which is always false. The check never fired, and a supply that was switched off reported the voltage it would have had if enabled. Callers cannot tell "off" from "on at this voltage", so anything reading a rail back to confirm it came up gets a false confirmation. Found by inspection while bringing up another Ingenic target. Not run on X1000 hardware as I do not have one of these devices. Change-Id: I19fc89ff33cce047160d826ce2344cc34b26e224 Co-Authored-By: Claude Opus 4.8 --- firmware/drivers/axp-2101.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/drivers/axp-2101.c b/firmware/drivers/axp-2101.c index 7cac31eb9c..a0797d72f1 100644 --- a/firmware/drivers/axp-2101.c +++ b/firmware/drivers/axp-2101.c @@ -355,7 +355,7 @@ int axp2101_supply_get_voltage(int supply) int r = i2c_reg_read1(AXP_PMU_BUS, AXP_PMU_ADDR, info->en_reg); if(r < 0) return AXP2101_SUPPLY_DISABLED; - if(r & (1 << info->en_bit) == 0) + if((r & (1 << info->en_bit)) == 0) return AXP2101_SUPPLY_DISABLED; } From fae5c8d0676ae785810e734184cbf52944b133b1 Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Sun, 9 Aug 2026 23:21:34 +0100 Subject: [PATCH 066/408] x1000: don't close fd 0 when a bootloader backup fails early backup_bootloader() and restore_bootloader() initialise `fd = 0` and their error paths close it unconditionally. Any failure before the file is opened updater_init(), or the size check between them therefore reaches close(0) and closes whatever fd 0 happens to be. On a native target that is whatever the filesystem layer handed out first, so an unrelated open file is closed and the damage surfaces somewhere else entirely. Initialise to -1 instead, which is the value close() ignores. Found by inspection while bringing up another Ingenic target. Not run on X1000 hardware. Change-Id: Ib043f79f0b7edc7c70ccd77cbff54e264ed2b2de Co-Authored-By: Claude Opus 4.8 --- firmware/target/mips/ingenic_x1000/installer-x1000.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firmware/target/mips/ingenic_x1000/installer-x1000.c b/firmware/target/mips/ingenic_x1000/installer-x1000.c index 769371d29d..70ed270408 100644 --- a/firmware/target/mips/ingenic_x1000/installer-x1000.c +++ b/firmware/target/mips/ingenic_x1000/installer-x1000.c @@ -237,7 +237,7 @@ int install_bootloader(const char* filename) int backup_bootloader(const char* filename) { - int rc, fd = 0; + int rc, fd = -1; struct updater u; rc = updater_init(&u); @@ -281,7 +281,7 @@ int backup_bootloader(const char* filename) int restore_bootloader(const char* filename) { - int rc, fd = 0; + int rc, fd = -1; struct updater u; rc = updater_init(&u); From 233ea05ea2b484fc797f6ed6992e8d2f02678840 Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Sun, 9 Aug 2026 23:21:34 +0100 Subject: [PATCH 067/408] x1000: set the NAND driver's init guard so setup runs once nand_init() guards its one-time setup with a static `inited` flag that nothing ever assigns, so every call re-runs the initialiser. That resets refcount to 0 on a driver another caller may already hold open, and the next nand_close() then decrements from zero and tears the driver down underneath its user. Assign the flag, and fix the caller that reaches this. On a failed open the installer's updater_cleanup() would call nand_close() against a driver nand_open() never took a reference on, because nand_open() only takes one once it has identified the chip; release the lock and drop the pointer instead. Change-Id: I88d54ac5bca9bcebb62fd9c6f50f1acf982a9a3b Co-Authored-By: Claude Opus 4.8 --- firmware/target/mips/ingenic_x1000/installer-x1000.c | 2 ++ firmware/target/mips/ingenic_x1000/nand-x1000.c | 1 + 2 files changed, 3 insertions(+) diff --git a/firmware/target/mips/ingenic_x1000/installer-x1000.c b/firmware/target/mips/ingenic_x1000/installer-x1000.c index 70ed270408..1b9ecf1f02 100644 --- a/firmware/target/mips/ingenic_x1000/installer-x1000.c +++ b/firmware/target/mips/ingenic_x1000/installer-x1000.c @@ -142,6 +142,8 @@ static int updater_init(struct updater* u) nand_lock(u->ndrv); rc = nand_open(u->ndrv); if(rc != NAND_SUCCESS) { + nand_unlock(u->ndrv); + u->ndrv = NULL; rc = IERR_NAND_OPEN; goto error; } diff --git a/firmware/target/mips/ingenic_x1000/nand-x1000.c b/firmware/target/mips/ingenic_x1000/nand-x1000.c index 39e06b1964..6961cbe58a 100644 --- a/firmware/target/mips/ingenic_x1000/nand-x1000.c +++ b/firmware/target/mips/ingenic_x1000/nand-x1000.c @@ -143,6 +143,7 @@ struct nand_drv* nand_init(void) static_nand_drv.scratch_buf = static_scratch_buf; static_nand_drv.page_buf = static_page_buf; static_nand_drv.refcount = 0; + inited = true; } return &static_nand_drv; From 6ad1fd074a284d934de4a8d0bbef02c1d6865506 Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Sun, 9 Aug 2026 23:21:48 +0100 Subject: [PATCH 068/408] usb: acknowledge SET_LINE_CODING in the serial driver usb_serial_control_request() copies the line coding and returns handled = true without calling usb_core_control_response(). usb_core only answers requests a driver declines, so returning handled makes the status stage the driver's responsibility and nothing sends it. The host sees the request accepted and then times out waiting for status. Every other branch in this function already responds but this one was missed. Affects any target building USB_ENABLE_SERIAL. The symptom is a CDC console that enumerates and then stalls when a terminal opens it and sets the line rate. Change-Id: Icd97805971c68ce3a599cf01e9f6aa103c61c2ec Co-Authored-By: Claude Opus 4.8 --- firmware/usbstack/usb_serial.c | 1 + 1 file changed, 1 insertion(+) diff --git a/firmware/usbstack/usb_serial.c b/firmware/usbstack/usb_serial.c index 4820d66d62..a9c06ff34d 100644 --- a/firmware/usbstack/usb_serial.c +++ b/firmware/usbstack/usb_serial.c @@ -288,6 +288,7 @@ static bool usb_serial_control_request(struct usb_ctrlrequest* req, uint8_t* req { /* Receive line coding into local copy */ memcpy(line_coding.raw, reqdata, sizeof(struct cdc_line_coding)); + usb_core_control_response(USB_CONTROL_ACK, NULL, 0); handled = true; } } From ef20bc4c7877d2397150dbbeecb6576ed90ba408 Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Sun, 9 Aug 2026 23:22:25 +0100 Subject: [PATCH 069/408] usb: keep the USB Serial setting across connects Debug -> USB Serial calls usb_core_enable_driver(USB_DRIVER_SERIAL, ...), which sets a flag inside usb_core. usb_core_init() clears every driver's enabled flag on each connect and runs after the toggle, so the choice is gone before any descriptor is built: the menu reports the new state and nothing changes. Hold the setting in firmware/usb.c and reapply it from usb_configure_drivers() on each connect, which is where the other drivers are already configured. usb_set_serial()/usb_get_serial() sit alongside usb_set_hid(), which works this way for the same reason. toggle_usb_core_driver() goes with it: toggle_usb_serial() was its only caller, and a generic wrapper around a call that cannot persist is not worth keeping. Affects any target building USB_ENABLE_SERIAL. Change-Id: I171a532d23cc9cf0efe0a01c2836b69846333a4d Co-Authored-By: Claude Opus 4.8 --- apps/debug_menu.c | 17 ++++++----------- firmware/export/usb.h | 5 +++++ firmware/usb.c | 22 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/apps/debug_menu.c b/apps/debug_menu.c index 2a0f182ed2..031845f32f 100644 --- a/apps/debug_menu.c +++ b/apps/debug_menu.c @@ -2537,20 +2537,15 @@ static bool dbg_talk(void) #ifdef HAVE_USBSTACK #if (defined(ROCKBOX_HAS_LOGF) && defined(USB_ENABLE_SERIAL)) -static bool toggle_usb_core_driver(int driver, char *msg) -{ - bool enabled = !usb_core_driver_enabled(driver); - - usb_core_enable_driver(driver,enabled); - splashf(HZ, "%s %s", msg, enabled ? "enabled" : "disabled"); - - return false; -} - #ifdef USB_ENABLE_SERIAL static bool toggle_usb_serial(void) { - return toggle_usb_core_driver(USB_DRIVER_SERIAL, "USB Serial"); + bool enabled = !usb_get_serial(); + + usb_set_serial(enabled); + splashf(HZ, "USB Serial %s", enabled ? "enabled" : "disabled"); + + return false; } #endif /* USB_ENABLE_SERIAL */ #endif diff --git a/firmware/export/usb.h b/firmware/export/usb.h index aa1e25c93b..de48c890f9 100644 --- a/firmware/export/usb.h +++ b/firmware/export/usb.h @@ -272,6 +272,11 @@ void usb_firewire_connect_event(void); void usb_set_hid(bool enable); #endif +#ifdef USB_ENABLE_SERIAL +void usb_set_serial(bool enable); +bool usb_get_serial(void); +#endif + #if defined(USB_ENABLE_AUDIO) || defined(HAVE_HOST_USB_AUDIO) /* Select when the USB Audio (DAC) function is active. Values follow the * usb_audio setting: 0 never, 1 always, 2 while charge-only, 3 while diff --git a/firmware/usb.c b/firmware/usb.c index 1e91efd96b..c44a7df7fc 100644 --- a/firmware/usb.c +++ b/firmware/usb.c @@ -117,6 +117,9 @@ static struct event_queue usb_queue SHAREDBSS_ATTR; #ifdef USB_ENABLE_HID static bool usb_hid = true; #endif +#ifdef USB_ENABLE_SERIAL +static bool usb_serial = false; +#endif #ifdef USB_ENABLE_AUDIO static int usb_audio = 0; #endif @@ -215,6 +218,9 @@ static inline void usb_configure_drivers(int for_state) usb_core_enable_driver(USB_DRIVER_HID, true); #endif /* USB_ENABLE_CHARGING_ONLY */ #endif /* USB_ENABLE_HID */ +#ifdef USB_ENABLE_SERIAL + usb_core_enable_driver(USB_DRIVER_SERIAL, usb_serial); +#endif #ifdef USB_ENABLE_AUDIO usb_core_enable_driver(USB_DRIVER_AUDIO, (usb_audio == 1) || (usb_audio == 2)); // while "always" or "only in charge-only mode" #endif /* USB_ENABLE_AUDIO */ @@ -237,6 +243,9 @@ static inline void usb_configure_drivers(int for_state) #ifdef USB_ENABLE_HID usb_core_enable_driver(USB_DRIVER_HID, usb_hid); #endif +#ifdef USB_ENABLE_SERIAL + usb_core_enable_driver(USB_DRIVER_SERIAL, usb_serial); +#endif #ifdef USB_ENABLE_AUDIO usb_core_enable_driver(USB_DRIVER_AUDIO, (usb_audio == 1) || (usb_audio == 3)); // while "always" or "only in mass-storage mode" #endif /* USB_ENABLE_AUDIO */ @@ -890,6 +899,19 @@ void usb_set_hid(bool enable) } #endif /* USB_ENABLE_HID */ +#ifdef USB_ENABLE_SERIAL +void usb_set_serial(bool enable) +{ + usb_serial = enable; + usb_core_enable_driver(USB_DRIVER_SERIAL, usb_serial); +} + +bool usb_get_serial(void) +{ + return usb_serial; +} +#endif /* USB_ENABLE_SERIAL */ + #ifdef USB_ENABLE_AUDIO void usb_set_audio(int value) { From 444c9ce4bd692f2c992790bc42a40018cc76aed3 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Tue, 11 Aug 2026 12:18:28 +0100 Subject: [PATCH 070/408] manual: add touchscreen settings section Change-Id: I5b9196fb79e7b67a4ac013fe10b8ad462cb8bfa8 --- manual/configure_rockbox/display_options.tex | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/manual/configure_rockbox/display_options.tex b/manual/configure_rockbox/display_options.tex index 4b48b768a9..f8b1344961 100755 --- a/manual/configure_rockbox/display_options.tex +++ b/manual/configure_rockbox/display_options.tex @@ -287,4 +287,38 @@ uses a different codepage and you are getting garbled extended characters you should adjust this settings. In most cases sticking to ``ISO-8859-1'' would be sufficient. +% + \opt{touchscreen}{ + \item[Touchscreen Settings.] + Options related to touchscreen behavior. + \begin{description} + \item[Touchscreen Mode.] + Rockbox supports two touchscreen modes. \setting{Absolute Point} behaves + like a normal touchscreen and allows you to select items by tapping on + them directly. \setting{3x3 Grid} divides the touchscreen into thirds to + emulate a grid of 9 buttons: eg. tapping on the center of the screen is + interpreted as a virtual ``center'' button. + \note{Some parts of the UI only support \setting{3x3 Grid} mode and will + fall back to it even if \setting{Absolute Point} mode is selected. In + particular, \setting{3x3 Grid} mode will be used in the WPS unless the + theme supports touch interaction; the default ``Cabbie v2.0'' theme + does not include touch support.} + \item[Line Padding In Lists.] + This option affects the amount of padding added to list entries, which + makes them larger on screen and easier to select with a touchscreen. + \setting{Auto} will calculate a reasonable amount of padding based on + the standard line height and the screen DPI. \setting{Off} disables the + feature and will not add any padding. Other values allow you to set the + number of pixels of padding to be added to the standard line height. + \item[Calibrate.] + Allows you to change the touchscreen calibration by touching on three + points on the screen to correct for any offset or rotation of the touch + overlay compared to the display. This calibration method is useful for + older devices that use a physically separate touch overlay panels, which + may be slightly mis-aligned to the display. + \item[Reset Calibration.] + Select this option to clear calibration data set by the + \setting{Calibrate} option. + \end{description} + } \end{description} From fd8d6f10a1581492619a765a5cb9eead20314e3b Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 11 Aug 2026 07:42:43 -0400 Subject: [PATCH 071/408] synopsys-dwmac: Bugfixes * Dedicated FIFO mode requires a non-zero multi-count for periodic INs * Add ISO frame polarity * Correct max packet size for high-speed ISOC operation Change-Id: I2c0d40b5f8d0e1e4cf43369631f17c7f80c6fab2 --- firmware/drivers/usb-designware.c | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/firmware/drivers/usb-designware.c b/firmware/drivers/usb-designware.c index 3a46a367ee..d6030caceb 100644 --- a/firmware/drivers/usb-designware.c +++ b/firmware/drivers/usb-designware.c @@ -708,6 +708,12 @@ static void usb_dw_epstart(int epnum, enum usb_dw_epdir epdir, #endif #ifdef USB_DW_SHARED_FIFO eptsiz |= MCCNT((ep_periodic_msk >> epnum) & 1); +#else + /* Dedicated FIFO mode still requires a non-zero multi-count for + * periodic IN endpoints. MCCNT(0) transmits no packets. */ + uint32_t eptype = (DWC_DIEPCTL(epnum) >> 18) & 0x3; + if (eptype == EPTYP_ISOCHRONOUS || eptype == EPTYP_INTERRUPT) + eptsiz |= MCCNT(packets); /* rarely, if ever, not 1 */ #endif } @@ -722,7 +728,18 @@ static void usb_dw_epstart(int epnum, enum usb_dw_epdir epdir, DWC_EPDMA(epnum, epdir) = USB_DW_PHYSADDR((uint32_t)buf); #endif DWC_EPTSIZ(epnum, epdir) = eptsiz; - DWC_EPCTL(epnum, epdir) |= EPENA | nak; + if (((DWC_EPCTL(epnum, epdir) >> 18) & 0x3) == EPTYP_ISOCHRONOUS) + { + /* Schedule the transfer for the next frame. */ + if ((DWC_DSTS >> 8) & 1) + DWC_EPCTL(epnum, epdir) |= EPENA | nak | SETD0PIDEF; + else + DWC_EPCTL(epnum, epdir) |= EPENA | nak | SETD1PIDOF; + } + else + { + DWC_EPCTL(epnum, epdir) |= EPENA | nak; + } #ifdef USB_DW_ARCH_SLAVE /* Enable interrupts to start pushing data into the FIFO */ @@ -1444,7 +1461,7 @@ void usb_drv_ep_init(const struct usb_drv_ep_alloc_ctx* ctx, int ep) { if(type == EPTYP_ISOCHRONOUS) { - mps = 1023; + mps = usb_drv_port_speed() ? 1024 : 1023; } else { From 842492d77bacf5755aca14f2fa00fe8690b09cc0 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Mon, 10 Aug 2026 20:51:48 +0300 Subject: [PATCH 072/408] hiby: r1_patcher: add macOS support Change-Id: If9f11fc7fbac87275325f0f892c2546184714d0b --- tools/r1_patcher/r1_patcher.sh | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/tools/r1_patcher/r1_patcher.sh b/tools/r1_patcher/r1_patcher.sh index b82a80457d..e70e9b20a9 100755 --- a/tools/r1_patcher/r1_patcher.sh +++ b/tools/r1_patcher/r1_patcher.sh @@ -1,7 +1,10 @@ #!/bin/bash -#Prerequisites: -#sudo apt update && sudo apt install -y p7zip-full squashfs-tools genisoimage coreutils +# Linux Prerequisites: +# sudo apt update && sudo apt install -y p7zip-full squashfs-tools genisoimage coreutils +# +# macOS Prerequisites: +# brew install p7zip squashfs cdrtools coreutils # Fail fast on any error, undefined variable, or failed pipeline set -euo pipefail @@ -9,6 +12,28 @@ set -euo pipefail # Report errors with line number and the failing command trap 'echo "ERROR at line ${LINENO}: ${BASH_COMMAND}" >&2' ERR +# ============================================================================== +# macOS Compatibility Shim +# Map missing/incompatible GNU tools to their Homebrew equivalents via functions +# ============================================================================== +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { grealpath "$@"; } + stat() { gstat "$@"; } + split() { gsplit "$@"; } + md5sum() { gmd5sum "$@"; } + genisoimage() { mkisofs "$@"; } + + # Quick sanity check for macOS users + for cmd in grealpath gstat gsplit gmd5sum mkisofs 7z unsquashfs mksquashfs; do + if ! command -v "$cmd" &> /dev/null; then + echo "ERROR: Missing macOS dependency for '$cmd'." >&2 + echo "Please run: brew install p7zip squashfs cdrtools coreutils" >&2 + exit 1 + fi + done +fi +# ============================================================================== + usage() { echo 'Usage:' >&2 echo ' ./r1_patcher.sh r1.upt bootloader.r1' >&2 From 0ca22b9a71f5a5c3b89bb3f36d08d0c7e4b6c111 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Fri, 25 Nov 2022 14:13:29 +0000 Subject: [PATCH 073/408] touchscreen: Improved kinetic scrolling in lists Kinetic scrolling now accelerates when repeatedly swiping over the screen, so you can scroll through large lists easily without using the scrollbar. Press & hold to slow down and stop. Menu items can no longer be selected during a kinetic scroll event, to reduce the chance of accidental selections. Change-Id: I05b7d5fe6681a9edf53096b1fa265442fb31bf33 --- apps/gui/bitmap/list.c | 83 +++++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 17 deletions(-) diff --git a/apps/gui/bitmap/list.c b/apps/gui/bitmap/list.c index 74bdea2f64..be0267fcc5 100644 --- a/apps/gui/bitmap/list.c +++ b/apps/gui/bitmap/list.c @@ -543,12 +543,22 @@ static int swipe_scroll(struct gui_synclist *gui_list, int delta) /* these could possibly be configurable */ /* the lower the smoother */ #define RELOAD_INTERVAL (HZ/25) -/* the higher the earler the list stops */ -#define DECELERATION (1000*RELOAD_INTERVAL/HZ) +/* deceleration factors: new velocity = P * old velocity * - K */ +#define DECEL_P (RELOAD_INTERVAL * 1) / (2 * HZ) +#define DECEL_K (RELOAD_INTERVAL * 2 * LCD_HEIGHT / HZ) +/* fast deceleration when the screen is pressed */ +#define FINGER_DECEL_P (RELOAD_INTERVAL * 2) / (1 * HZ) +#define FINGER_DECEL_K (RELOAD_INTERVAL * (LCD_HEIGHT * 4) / HZ) +/* finger deceleration does not kick in until the touch duration exceeds this */ +#define FINGER_DECEL_GRACE (HZ/4) +/* fraction used to multiply gesture velocity before accumulating it */ +#define VELOCITY_RESIST 3 / 4 struct kinetic_cb_data { struct gui_synclist *list; int velocity; + int subpixel_accum; + long finger_tick; }; struct kinetic { @@ -563,7 +573,11 @@ static struct gesture_vel list_gvel; static void kinetic_stop_scrolling(struct kinetic *k, struct gui_synclist *list) { if (k->cb_data.list == list) + { + k->cb_data.subpixel_accum = 0; + k->cb_data.velocity = 0; timeout_cancel(&k->tmo); + } } /* helper for gui/list.c to cancel scrolling if a normal button event comes */ @@ -586,7 +600,11 @@ static int kinetic_callback(struct timeout *tmo) return 0; /* ds = v*dt */ - int pixel_diff = data->velocity * RELOAD_INTERVAL / HZ; + data->subpixel_accum += 100 * data->velocity * RELOAD_INTERVAL / HZ; + + int pixel_diff = data->subpixel_accum / 100; + data->subpixel_accum %= 100; + int action = swipe_scroll(list, pixel_diff); if (action == ACTION_REDRAW) { @@ -594,14 +612,33 @@ static int kinetic_callback(struct timeout *tmo) button_queue_post(BUTTON_REDRAW, 0); } - /* apply deceleration */ - int old_sign = SIGN(data->velocity); - data->velocity -= SIGN(data->velocity) * DECELERATION; - if (SIGN(data->velocity) != old_sign) + /* calculate and apply deceleration */ + int sign = SIGN(data->velocity); + int absvel = abs(data->velocity); + int decel; + + if (data->finger_tick != 0 && + !TIME_BEFORE(current_tick, data->finger_tick + FINGER_DECEL_GRACE)) + { + decel = data->velocity * FINGER_DECEL_P + sign * FINGER_DECEL_K; + } + else + { + decel = data->velocity * DECEL_P + sign * DECEL_K; + } + + /* + * Ensure velocity is smoothly reduced to zero to avoid jerkiness. + */ + if (absvel < 3) data->velocity = 0; + else if (absvel < sign*decel) + data->velocity = data->velocity / 3; + else + data->velocity -= decel; /* stop scrolling if we didn't move, it means we hit the end */ - if (list->y_pos == list->scroll_base_y) + if (list->y_pos == list->scroll_base_y && pixel_diff != 0) data->velocity = 0; else /* update base y since our scroll distance doesn't accumulate. */ @@ -625,7 +662,7 @@ static bool kinetic_start_scrolling(struct kinetic *k, struct gui_synclist *list return false; k->cb_data.list = list; - k->cb_data.velocity = yvel; + k->cb_data.velocity += yvel * VELOCITY_RESIST; list->scroll_mode = SCROLL_KINETIC; list->scroll_base_y = list->y_pos; @@ -728,6 +765,11 @@ unsigned gui_synclist_do_touchscreen(struct gui_synclist *list) int action = ACTION_NONE; int click_loc; + if (action_gesture_is_pressed()) + kinetic.cb_data.finger_tick = gevent.start_tick; + else + kinetic.cb_data.finger_tick = 0; + switch (gevent.id) { case GESTURE_NONE: @@ -737,7 +779,15 @@ unsigned gui_synclist_do_touchscreen(struct gui_synclist *list) case GESTURE_TAP: case GESTURE_LONG_PRESS: - _gui_synclist_stop_kinetic_scrolling(list); + /* In kinetic mode taps and presses only decelerate scrolling. + * User needs to wait until scrolling stops to select an item. */ + if (list->scroll_mode == SCROLL_KINETIC) + { + if (gevent.id != GESTURE_NONE) + gesture_vel_reset(&list_gvel); + break; + } + click_loc = get_click_location(list, gevent.x, gevent.y); if (click_loc & LIST) { @@ -789,9 +839,7 @@ unsigned gui_synclist_do_touchscreen(struct gui_synclist *list) break; case GESTURE_DRAGSTART: - _gui_synclist_stop_kinetic_scrolling(list); gesture_vel_reset(&list_gvel); - list->scroll_base_y = list->y_pos; /* fallthrough */ case GESTURE_DRAG: @@ -799,7 +847,9 @@ unsigned gui_synclist_do_touchscreen(struct gui_synclist *list) if (list->scroll_mode == SCROLL_NONE) { + list->scroll_base_y = list->y_pos; click_loc = get_click_location(list, gevent.ox, gevent.oy); + if (click_loc & SCROLLBAR) list->scroll_mode = SCROLL_BAR; else if (click_loc & LIST) @@ -810,15 +860,14 @@ unsigned gui_synclist_do_touchscreen(struct gui_synclist *list) action = scrollbar_scroll(list, gevent.y); else if (list->scroll_mode == SCROLL_SWIPE) action = swipe_scroll(list, gevent.y - gevent.oy); - break; case GESTURE_RELEASE: - if (list->scroll_mode != SCROLL_SWIPE || - !kinetic_start_scrolling(&kinetic, list)) - { + if (list->scroll_mode == SCROLL_BAR) + list->scroll_mode = SCROLL_NONE; + else if(!kinetic_start_scrolling(&kinetic, list) && + list->scroll_mode != SCROLL_KINETIC) list->scroll_mode = SCROLL_NONE; - } action_gesture_reset(); action = ACTION_REDRAW; From 814747492c0bc8dfb6a8ce196e338b12929b3d53 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Tue, 12 Jul 2022 23:21:56 +0100 Subject: [PATCH 074/408] touchscreen: Add flick gesture detector Add a low-overhead detector for flick gestures, which are quick swiping motions from the screen edges. Change-Id: I3ef2dad328fdf566923575d437376e100a2ab8df --- apps/gesture.c | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ apps/gesture.h | 20 ++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/apps/gesture.c b/apps/gesture.c index 752f007896..c12d3b7593 100644 --- a/apps/gesture.c +++ b/apps/gesture.c @@ -180,3 +180,59 @@ bool gesture_vel_get(struct gesture_vel *gv, int *xvel, int *yvel) return gv->cnt >= ARRAYLEN(gv->xsamp); } + +/* + * flick detector + */ + +int gesture_flick_get_in_vp(const struct gesture_event *gevt, + const struct viewport *vp) +{ + const int vp_height = vp ? vp->height : LCD_HEIGHT; + const int vp_width = vp ? vp->width : LCD_WIDTH; + + /* margin at edge where flicks must originate */ + const int margin = touchscreen_get_scroll_threshold() * 2; + + /* minimum distance from edge before a flick is reported */ + const int x_dist = 1 + vp_width/4; + const int y_dist = 1 + vp_height/4; + + /* a flick only triggers if the velocity along the main axis + * exceeds the minimum and the velocity on the other axis is + * below the maximum */ + const int xv_min = 2*vp_width; + const int xv_max = vp_width; + const int yv_min = 2*vp_height; + const int yv_max = vp_height; + + /* a flick is only detected if it occurs within this timeout */ + const long timeout = HZ/2; + + long dt = gevt->last_tick - gevt->start_tick; + if (dt <= 0 || dt > timeout) + return GESTURE_FLICK_NONE; + + int xvel = (gevt->x - gevt->ox) * HZ / dt; + int yvel = (gevt->y - gevt->oy) * HZ / dt; + + if (gevt->ox < margin && gevt->x >= x_dist && + xvel >= xv_min && abs(yvel) <= yv_max) + return GESTURE_FLICK_LEFT; + + if (gevt->ox >= LCD_WIDTH - margin && + gevt->x < LCD_WIDTH - x_dist && + xvel <= -xv_min && abs(yvel) <= yv_max) + return GESTURE_FLICK_RIGHT; + + if (gevt->oy < margin && gevt->y >= y_dist && + yvel >= yv_min && abs(xvel) <= xv_max) + return GESTURE_FLICK_TOP; + + if (gevt->oy >= LCD_HEIGHT - margin && + gevt->y < LCD_HEIGHT - y_dist && + yvel <= -yv_min && abs(xvel) < xv_max) + return GESTURE_FLICK_BOTTOM; + + return GESTURE_FLICK_NONE; +} diff --git a/apps/gesture.h b/apps/gesture.h index a4fb5428dc..948d037d1f 100644 --- a/apps/gesture.h +++ b/apps/gesture.h @@ -133,4 +133,24 @@ void gesture_vel_reset(struct gesture_vel *gv); void gesture_vel_process(struct gesture_vel *gv, const struct touchevent *ev); bool gesture_vel_get(struct gesture_vel *gv, int *xvel, int *yvel); +/* Flick detection */ +enum gesture_flick_id +{ + /* The enum refers to originating edge: GESTURE_FLICK_LEFT + * means a flick from the left edge moving rightward. */ + GESTURE_FLICK_NONE = 0, + GESTURE_FLICK_TOP, + GESTURE_FLICK_BOTTOM, + GESTURE_FLICK_LEFT, + GESTURE_FLICK_RIGHT, +}; + +int gesture_flick_get_in_vp(const struct gesture_event *gevt, + const struct viewport *vp); + +static inline int gesture_flick_get(const struct gesture_event *gevt) +{ + return gesture_flick_get_in_vp(gevt, NULL); +} + #endif /* _GESTURE_H_ */ From a89e1f999d4e267d28d51e5133c56142b7a9b4e7 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Mon, 1 Dec 2025 20:46:58 +0000 Subject: [PATCH 075/408] touchscreen: Add flick detector to touchscreen test Change-Id: I9b41881e3e9a271f0532cc9c500e0511c606e895 --- apps/plugin.c | 4 +++ apps/plugin.h | 5 +++ apps/plugins/test_touchscreen.c | 54 +++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/apps/plugin.c b/apps/plugin.c index d06e9936e3..51b4478421 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -876,6 +876,10 @@ static const struct plugin_api rockbox_api = { /* new stuff at the end, sort into place next time the API gets incompatible */ font_measurestring, +#ifdef HAVE_TOUCHSCREEN + gesture_flick_get_in_vp, + gesture_flick_get, +#endif }; static int plugin_buffer_handle; diff --git a/apps/plugin.h b/apps/plugin.h index 7ccc4b1239..cc10371a35 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -1041,6 +1041,11 @@ struct plugin_api { the API gets incompatible */ int (*font_measurestring)(const unsigned char *str, size_t maxbytes, size_t maxwidth, int *w, int *h, int fontnum); +#ifdef HAVE_TOUCHSCREEN + int (*gesture_flick_get_in_vp)(const struct gesture_event *gevt, + const struct viewport *vp); + int (*gesture_flick_get)(const struct gesture_event *gevt); +#endif }; /* plugin header */ diff --git a/apps/plugins/test_touchscreen.c b/apps/plugins/test_touchscreen.c index af130c4016..3c9e559a45 100644 --- a/apps/plugins/test_touchscreen.c +++ b/apps/plugins/test_touchscreen.c @@ -287,6 +287,55 @@ static void tstest_velocity(void) } } +static void tstest_flick(void) +{ + struct gesture_event gevent; + + rb->splashf(HZ, "Flick test"); + rb->touchscreen_set_mode(TOUCHSCREEN_POINT); + + rb->lcd_clear_display(); + rb->lcd_update(); + + while (true) + { + int action = rb->get_custom_action(CONTEXT_PLUGIN, TIMEOUT_BLOCK, + get_context_map); + if (action == ACTION_TOUCHSCREEN) + { + rb->lcd_clear_display(); + rb->action_gesture_get_event(&gevent); + + switch (rb->gesture_flick_get(&gevent)) { + case GESTURE_FLICK_TOP: + rb->splashf(HZ, "top flick"); + break; + case GESTURE_FLICK_BOTTOM: + rb->splashf(HZ, "bottom flick"); + break; + case GESTURE_FLICK_LEFT: + rb->splashf(HZ, "left flick"); + break; + case GESTURE_FLICK_RIGHT: + rb->splashf(HZ, "right flick"); + break; + } + + rb->lcd_clear_display(); + rb->lcd_update(); + } + else if (action == ACTION_STD_CANCEL) + { + break; + } + else if (rb->default_event_handler(action) == SYS_USB_CONNECTED) + { + usb_exit = true; + break; + } + } +} + /* plugin entry point */ enum plugin_status plugin_start(const void* parameter) { @@ -299,6 +348,7 @@ enum plugin_status plugin_start(const void* parameter) "Pointing mode test", "Gesture test", "Velocity test", + "Flick test", "Quit"); while (true) @@ -322,6 +372,10 @@ enum plugin_status plugin_start(const void* parameter) tstest_velocity(); break; + case 4: + tstest_flick(); + break; + default: return PLUGIN_OK; } From c93c7bfdcb319025d0f19034c4e37b74997888d1 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Sat, 26 Nov 2022 21:43:17 +0000 Subject: [PATCH 076/408] touchscreen: Kinetic scrolling v2 Add configurable acceleration settings. There are 3 settings which are applied at different times during scrolling. "accel" is used to calculate the acceleration applied to the list based on the swipe speed. "decel" determines the deceleration force applied based on the list scroll speed, and "press" is an additional deceleration force applied if you are pressing on the screen. All settings have the general form: kinetic scroll accel: kinetic scroll decel: kinetic scroll press: where the formula for the acceleration is "accel = a0 + a1*vel" and "vel" is the input velocity (swipe speed for accel, list scroll speed for decel/press). The delay parameter is a value in milliseconds which acts as a grace period where the acceleration will *not* be applied. For example, if you specify 250ms for decel, then deceleration will not kick in until 250ms after scrolling starts. For press, the delay is based on the duration of the touchscreen press. All settings have sane default values and don't need to be configured to enable kinetic scrolling; it is always enabled. But the settings are there for those who want to tweak the behavior. Change-Id: I9723b496b2b5d3fb3643af60b169754fdba497f0 --- apps/gui/bitmap/list.c | 126 +++++++++++++++++++++++++++-------------- apps/gui/list.h | 30 ++++++++++ apps/settings.h | 4 ++ apps/settings_list.c | 84 +++++++++++++++++++++++++++ 4 files changed, 201 insertions(+), 43 deletions(-) diff --git a/apps/gui/bitmap/list.c b/apps/gui/bitmap/list.c index be0267fcc5..3d77afc929 100644 --- a/apps/gui/bitmap/list.c +++ b/apps/gui/bitmap/list.c @@ -41,6 +41,7 @@ #include "statusbar-skinned.h" #include "debug.h" #include "line.h" +#include "fixedpoint.h" #define ICON_PADDING 1 #define ICON_PADDING_S "1" @@ -540,25 +541,15 @@ static int swipe_scroll(struct gui_synclist *gui_list, int delta) */ #define SIGN(a) ((a) < 0 ? -1 : 1) -/* these could possibly be configurable */ -/* the lower the smoother */ #define RELOAD_INTERVAL (HZ/25) -/* deceleration factors: new velocity = P * old velocity * - K */ -#define DECEL_P (RELOAD_INTERVAL * 1) / (2 * HZ) -#define DECEL_K (RELOAD_INTERVAL * 2 * LCD_HEIGHT / HZ) -/* fast deceleration when the screen is pressed */ -#define FINGER_DECEL_P (RELOAD_INTERVAL * 2) / (1 * HZ) -#define FINGER_DECEL_K (RELOAD_INTERVAL * (LCD_HEIGHT * 4) / HZ) -/* finger deceleration does not kick in until the touch duration exceeds this */ -#define FINGER_DECEL_GRACE (HZ/4) -/* fraction used to multiply gesture velocity before accumulating it */ -#define VELOCITY_RESIST 3 / 4 +#define RELOAD_INTERVAL_FP ((RELOAD_INTERVAL << LIST_KINETIC_FRACBITS) / HZ) struct kinetic_cb_data { struct gui_synclist *list; - int velocity; - int subpixel_accum; - long finger_tick; + long velocity; + long distance; + long scroll_duration; + long press_duration; }; struct kinetic { @@ -570,12 +561,31 @@ struct kinetic { static struct kinetic kinetic; static struct gesture_vel list_gvel; +const struct list_kinetic_scroll_settings list_kinetic_scroll_accel_default = { + .a0 = 1000 << LIST_KINETIC_FRACBITS, + .a1 = 1 << (LIST_KINETIC_FRACBITS - 1), + /* delay is ignored for this one */ +}; + +const struct list_kinetic_scroll_settings list_kinetic_scroll_decel_default = { + .a0 = 3000 << LIST_KINETIC_FRACBITS, + .a1 = 1 << (LIST_KINETIC_FRACBITS - 1), + .delay = 125 * HZ / 1000, +}; + +const struct list_kinetic_scroll_settings list_kinetic_scroll_press_default = { + .a0 = 35000 << LIST_KINETIC_FRACBITS, + .a1 = 4 << LIST_KINETIC_FRACBITS, + .delay = 250 * HZ / 1000, +}; + static void kinetic_stop_scrolling(struct kinetic *k, struct gui_synclist *list) { if (k->cb_data.list == list) { - k->cb_data.subpixel_accum = 0; + k->cb_data.list = NULL; k->cb_data.velocity = 0; + k->cb_data.distance = 0; timeout_cancel(&k->tmo); } } @@ -590,22 +600,39 @@ void _gui_synclist_stop_kinetic_scrolling(struct gui_synclist *list) } } +static long kinetic_calc_accel(long input, long duration, + const struct list_kinetic_scroll_settings *param) +{ + if (duration < param->delay) + return 0; + + long x1 = input < 0 ? -input : input; + long r = fp_mul(param->a1, x1, LIST_KINETIC_FRACBITS) + + param->a0 + (1 << (LIST_KINETIC_FRACBITS - 1)); + + return SIGN(input) * r; +} + static int kinetic_callback(struct timeout *tmo) { struct kinetic_cb_data *data = (struct kinetic_cb_data*)tmo->data; struct gui_synclist *list = data->list; + int pixel_diff, action; /* deal with cancellation */ - if (list->scroll_mode != SCROLL_KINETIC) + if (!list || list->scroll_mode != SCROLL_KINETIC) return 0; - /* ds = v*dt */ - data->subpixel_accum += 100 * data->velocity * RELOAD_INTERVAL / HZ; + long abs_vel = data->velocity < 0 ? -data->velocity : data->velocity; + long vel_sgn = SIGN(data->velocity); - int pixel_diff = data->subpixel_accum / 100; - data->subpixel_accum %= 100; + data->distance += fp_mul(abs_vel, RELOAD_INTERVAL_FP, LIST_KINETIC_FRACBITS); + data->scroll_duration += RELOAD_INTERVAL; - int action = swipe_scroll(list, pixel_diff); + pixel_diff = data->distance >> LIST_KINETIC_FRACBITS; + data->distance -= pixel_diff << LIST_KINETIC_FRACBITS; + + action = swipe_scroll(list, pixel_diff * vel_sgn); if (action == ACTION_REDRAW) { /* force the list to redraw */ @@ -613,29 +640,23 @@ static int kinetic_callback(struct timeout *tmo) } /* calculate and apply deceleration */ - int sign = SIGN(data->velocity); - int absvel = abs(data->velocity); - int decel; - - if (data->finger_tick != 0 && - !TIME_BEFORE(current_tick, data->finger_tick + FINGER_DECEL_GRACE)) - { - decel = data->velocity * FINGER_DECEL_P + sign * FINGER_DECEL_K; - } - else - { - decel = data->velocity * DECEL_P + sign * DECEL_K; - } + long abs_decel = 0; + abs_decel += kinetic_calc_accel(abs_vel, data->scroll_duration, + &global_settings.kinetic_scroll_decel); + abs_decel += kinetic_calc_accel(abs_vel, data->press_duration, + &global_settings.kinetic_scroll_press); + abs_decel = fp_mul(abs_decel, RELOAD_INTERVAL_FP, LIST_KINETIC_FRACBITS); /* - * Ensure velocity is smoothly reduced to zero to avoid jerkiness. + * Ensure velocity is smoothly reduced to zero to avoid jerky + * scrolling near the end of scrolling. */ - if (absvel < 3) + if (abs_vel < (3 << LIST_KINETIC_FRACBITS)) data->velocity = 0; - else if (absvel < sign*decel) - data->velocity = data->velocity / 3; + else if (abs_vel < abs_decel) + data->velocity /= 3; else - data->velocity -= decel; + data->velocity -= SIGN(data->velocity) * abs_decel; /* stop scrolling if we didn't move, it means we hit the end */ if (list->y_pos == list->scroll_base_y && pixel_diff != 0) @@ -661,8 +682,27 @@ static bool kinetic_start_scrolling(struct kinetic *k, struct gui_synclist *list if (yvel == 0) return false; + long yvel_fp = yvel << LIST_KINETIC_FRACBITS; + if (list->scroll_mode == SCROLL_KINETIC) + { + long accel = kinetic_calc_accel(k->cb_data.velocity, LONG_MAX, + &global_settings.kinetic_scroll_accel); + accel = fp_mul(accel, RELOAD_INTERVAL_FP, LIST_KINETIC_FRACBITS); + + /* the acceleration is in the direction of the swipe */ + if (SIGN(accel) != SIGN(yvel_fp)) + accel = -accel; + + yvel_fp += accel; + } + k->cb_data.list = list; - k->cb_data.velocity += yvel * VELOCITY_RESIST; + k->cb_data.velocity += yvel_fp; + if (list->scroll_mode != SCROLL_KINETIC) + { + k->cb_data.distance = 0; + k->cb_data.scroll_duration = 0; + } list->scroll_mode = SCROLL_KINETIC; list->scroll_base_y = list->y_pos; @@ -766,9 +806,9 @@ unsigned gui_synclist_do_touchscreen(struct gui_synclist *list) int click_loc; if (action_gesture_is_pressed()) - kinetic.cb_data.finger_tick = gevent.start_tick; + kinetic.cb_data.press_duration = gevent.last_tick - gevent.start_tick; else - kinetic.cb_data.finger_tick = 0; + kinetic.cb_data.press_duration = -1; switch (gevent.id) { diff --git a/apps/gui/list.h b/apps/gui/list.h index ad31180c4f..534558c0bc 100644 --- a/apps/gui/list.h +++ b/apps/gui/list.h @@ -189,6 +189,36 @@ struct gui_synclist #endif }; +#ifdef HAVE_TOUCHSCREEN +#define LIST_KINETIC_FRACBITS 8 + +struct list_kinetic_scroll_settings +{ + /* Coefficients for a polynomial a1*x + a0, in fixed point. + * x is the kinetic scroll speed in pixels/sec (always positive) */ + long a0; + long a1; + + /* Delay (in ticks) during which the acceleration is not applied. */ + long delay; +}; + +/* Acceleration applied to the scroll velocity when a swipe occurs during + * kinetic scrolling. This is needed to counteract the deceleration term + * for high scrolling speeds, since the user can only input a low constant + * acceleration from swiping alone. Delay is ignored. */ +extern const struct list_kinetic_scroll_settings list_kinetic_scroll_accel_default; + +/* Deceleration applied to the scroll velocity during kinetic scrolling. + * The delay occurs right at the beginning of scrolling, eg. a delay of + * 1 second means deceleration starts 1 second after scrolling. */ +extern const struct list_kinetic_scroll_settings list_kinetic_scroll_decel_default; + +/* Deceleration applied during kinetic scrolling when the screen is + * pressed for longer than the delay interval (motion is not required). + * This is additive with list_kinetic_scroll_decel. */ +extern const struct list_kinetic_scroll_settings list_kinetic_scroll_press_default; +#endif extern void list_init(void); diff --git a/apps/settings.h b/apps/settings.h index 0fc07a2413..35af24c97d 100644 --- a/apps/settings.h +++ b/apps/settings.h @@ -31,6 +31,7 @@ #include "button.h" #include "audio.h" #include "dsp_proc_settings.h" +#include "gui/list.h" struct opt_items { unsigned const char* string; @@ -799,6 +800,9 @@ struct user_settings #ifdef HAVE_TOUCHSCREEN int touch_mode; struct touchscreen_parameter ts_calibration_data; + struct list_kinetic_scroll_settings kinetic_scroll_accel; + struct list_kinetic_scroll_settings kinetic_scroll_decel; + struct list_kinetic_scroll_settings kinetic_scroll_press; #endif #ifdef HAVE_PITCHCONTROL diff --git a/apps/settings_list.c b/apps/settings_list.c index 6821671d05..958e61f07f 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -42,6 +42,7 @@ #include "open_plugin.h" #include "misc.h" #include "playback.h" +#include "fixedpoint.h" #ifdef HAVE_REMOTE_LCD #include "lcd-remote.h" #endif @@ -950,6 +951,77 @@ static void tsc_set_default(void* setting, void* defaultval) { memcpy(setting, defaultval, sizeof(struct touchscreen_parameter)); } + +static void list_kinetic_load_from_cfg(void *setting, char *value) +{ + struct list_kinetic_scroll_settings *param = setting; + long vals[4] = { 0, 0, 0 }; + int count = 0; + + while (*value && count < 3) + { + while (isspace (*value)) + value++; + + int num = atoi(value); + while (!isspace(*value)) + value++; + + if (count == 2) + /* delay in milliseconds */ + vals[count] = HZ * num / 1000; + else + { + /* polynomial coefficients in 1/100th scale */ + int sign = 1; + if (num < 0) + { + num = -num; + sign = -1; + } + + int frac = num % 100; + num /= 100; + + num <<= LIST_KINETIC_FRACBITS; + frac <<= LIST_KINETIC_FRACBITS; + vals[count] = num + fp_div(frac, 100, LIST_KINETIC_FRACBITS); + vals[count] *= sign; + } + + count++; + } + + param->a1 = vals[0]; + param->a0 = vals[1]; + param->delay = vals[2]; +} + +static char *list_kinetic_write_to_cfg(void *setting, char *buf, int buf_len) +{ + struct list_kinetic_scroll_settings *param = setting; + long vals[2] = { param->a1, param->a0 }; + + for (int i = 0; i < 2; ++i) + { + long num = vals[i] >> LIST_KINETIC_FRACBITS; + long frac = vals[i] - (num << LIST_KINETIC_FRACBITS); + vals[i] = (num * 100) + (frac * 100) / (1 << LIST_KINETIC_FRACBITS); + } + + snprintf(buf, buf_len, "%ld %ld %ld", + vals[0], vals[1], 1000 * param->delay / HZ); + return buf; +} + +static bool list_kinetic_is_default(void *setting, void *defaultval) +{ + return memcmp(setting, defaultval, sizeof(struct list_kinetic_scroll_settings)) != 0; +} +static void list_kinetic_set_default(void *setting, void *defaultval) +{ + memcpy(setting, defaultval, sizeof(struct list_kinetic_scroll_settings)); +} #endif static void start_in_callback(int var) @@ -2353,6 +2425,18 @@ const struct settings_list settings[] = { &default_calibration_parameters, "touchscreen calibration", tsc_load_from_cfg, tsc_write_to_cfg, tsc_is_changed, tsc_set_default), + CUSTOM_SETTING(0, kinetic_scroll_accel, -1, + &list_kinetic_scroll_accel_default, "kinetic scroll accel", + list_kinetic_load_from_cfg, list_kinetic_write_to_cfg, + list_kinetic_is_default, list_kinetic_set_default), + CUSTOM_SETTING(0, kinetic_scroll_decel, -1, + &list_kinetic_scroll_decel_default, "kinetic scroll decel", + list_kinetic_load_from_cfg, list_kinetic_write_to_cfg, + list_kinetic_is_default, list_kinetic_set_default), + CUSTOM_SETTING(0, kinetic_scroll_press, -1, + &list_kinetic_scroll_press_default, "kinetic scroll press", + list_kinetic_load_from_cfg, list_kinetic_write_to_cfg, + list_kinetic_is_default, list_kinetic_set_default), #endif OFFON_SETTING(0, prevent_skip, LANG_PREVENT_SKIPPING, false, "prevent track skip", NULL), OFFON_SETTING(0, rewind_across_tracks, LANG_REWIND_ACROSS_TRACKS, false, "rewind across tracks", NULL), From 0e0982cb293ec6238cbd99f6ff2055b23c5decd1 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Sun, 27 Nov 2022 01:48:10 +0000 Subject: [PATCH 077/408] touchscreen: Kinetic scrolling v3 Tune accel/decel/press coefficients and add a braking force which kicks in when swiping in the opposite direction of scrolling. This is instead of the usual acceleration force. Change-Id: I2b220c98674933a6957d6982020406342cd019bd --- apps/gui/bitmap/list.c | 47 +++++++++++++++++++++++++++--------------- apps/gui/list.h | 29 +++++++++++++++----------- apps/settings.h | 1 + apps/settings_list.c | 4 ++++ 4 files changed, 52 insertions(+), 29 deletions(-) diff --git a/apps/gui/bitmap/list.c b/apps/gui/bitmap/list.c index 3d77afc929..ba155c0a16 100644 --- a/apps/gui/bitmap/list.c +++ b/apps/gui/bitmap/list.c @@ -561,22 +561,31 @@ struct kinetic { static struct kinetic kinetic; static struct gesture_vel list_gvel; +/* + * NOTE: defaults were chosen for the Shanling Q1, and may need + * tuning for other devices + */ + const struct list_kinetic_scroll_settings list_kinetic_scroll_accel_default = { - .a0 = 1000 << LIST_KINETIC_FRACBITS, - .a1 = 1 << (LIST_KINETIC_FRACBITS - 1), - /* delay is ignored for this one */ + .a0 = 200 << LIST_KINETIC_FRACBITS, /* 200.0 */ + .a1 = (6 << LIST_KINETIC_FRACBITS) / 5, /* 1.2 */ +}; + +const struct list_kinetic_scroll_settings list_kinetic_scroll_brake_default = { + .a0 = 200 << LIST_KINETIC_FRACBITS, /* 200.0 */ + .a1 = (1 << LIST_KINETIC_FRACBITS) / 2, /* 0.5 */ }; const struct list_kinetic_scroll_settings list_kinetic_scroll_decel_default = { - .a0 = 3000 << LIST_KINETIC_FRACBITS, - .a1 = 1 << (LIST_KINETIC_FRACBITS - 1), - .delay = 125 * HZ / 1000, + .a0 = 2000 << LIST_KINETIC_FRACBITS, /* 2000.0 */ + .a1 = (1 << LIST_KINETIC_FRACBITS) / 2, /* 0.5 */ + .delay = 125 * HZ / 1000, /* 125 ms */ }; const struct list_kinetic_scroll_settings list_kinetic_scroll_press_default = { - .a0 = 35000 << LIST_KINETIC_FRACBITS, - .a1 = 4 << LIST_KINETIC_FRACBITS, - .delay = 250 * HZ / 1000, + .a0 = 35000 << LIST_KINETIC_FRACBITS, /* 35000.0 */ + .a1 = 4 << LIST_KINETIC_FRACBITS, /* 4.0 */ + .delay = 250 * HZ / 1000, /* 250 ms */ }; static void kinetic_stop_scrolling(struct kinetic *k, struct gui_synclist *list) @@ -683,22 +692,26 @@ static bool kinetic_start_scrolling(struct kinetic *k, struct gui_synclist *list return false; long yvel_fp = yvel << LIST_KINETIC_FRACBITS; + int vel_sgn = SIGN(k->cb_data.velocity); + if (list->scroll_mode == SCROLL_KINETIC) { - long accel = kinetic_calc_accel(k->cb_data.velocity, LONG_MAX, - &global_settings.kinetic_scroll_accel); - accel = fp_mul(accel, RELOAD_INTERVAL_FP, LIST_KINETIC_FRACBITS); - - /* the acceleration is in the direction of the swipe */ - if (SIGN(accel) != SIGN(yvel_fp)) - accel = -accel; + long accel; + if (SIGN(yvel) == vel_sgn) + accel = kinetic_calc_accel(k->cb_data.velocity, LONG_MAX, + &global_settings.kinetic_scroll_accel); + else + accel = -kinetic_calc_accel(k->cb_data.velocity, LONG_MAX, + &global_settings.kinetic_scroll_brake); yvel_fp += accel; } k->cb_data.list = list; k->cb_data.velocity += yvel_fp; - if (list->scroll_mode != SCROLL_KINETIC) + /* also reset distance & scroll duration after a direction change */ + if (list->scroll_mode != SCROLL_KINETIC || + vel_sgn != SIGN(k->cb_data.velocity)) { k->cb_data.distance = 0; k->cb_data.scroll_duration = 0; diff --git a/apps/gui/list.h b/apps/gui/list.h index 534558c0bc..0e3a350ed7 100644 --- a/apps/gui/list.h +++ b/apps/gui/list.h @@ -203,20 +203,25 @@ struct list_kinetic_scroll_settings long delay; }; -/* Acceleration applied to the scroll velocity when a swipe occurs during - * kinetic scrolling. This is needed to counteract the deceleration term - * for high scrolling speeds, since the user can only input a low constant - * acceleration from swiping alone. Delay is ignored. */ +/* + * accel: Extra impulse applied when swiping in the direction of scrolling, + * proportional to the current velocity. This is what allows for high + * scroll speeds. Delay is ignored. + * + * brake: Braking impulse applied when swiping in the opposite direction of + * scrolling, which helps to change direction quickly even at high + * speed. Delay is ignored. + * + * decel: Deceleration applied during scrolling. The delay is measured from + * the start of scrolling or last change of direction. + * + * press: Deceleration applied while the screen is pressed (motion is not + * needed to trigger). Additive with "decel". The delay is measured + * from the start of the press. + */ extern const struct list_kinetic_scroll_settings list_kinetic_scroll_accel_default; - -/* Deceleration applied to the scroll velocity during kinetic scrolling. - * The delay occurs right at the beginning of scrolling, eg. a delay of - * 1 second means deceleration starts 1 second after scrolling. */ +extern const struct list_kinetic_scroll_settings list_kinetic_scroll_brake_default; extern const struct list_kinetic_scroll_settings list_kinetic_scroll_decel_default; - -/* Deceleration applied during kinetic scrolling when the screen is - * pressed for longer than the delay interval (motion is not required). - * This is additive with list_kinetic_scroll_decel. */ extern const struct list_kinetic_scroll_settings list_kinetic_scroll_press_default; #endif diff --git a/apps/settings.h b/apps/settings.h index 35af24c97d..f5d036f9da 100644 --- a/apps/settings.h +++ b/apps/settings.h @@ -801,6 +801,7 @@ struct user_settings int touch_mode; struct touchscreen_parameter ts_calibration_data; struct list_kinetic_scroll_settings kinetic_scroll_accel; + struct list_kinetic_scroll_settings kinetic_scroll_brake; struct list_kinetic_scroll_settings kinetic_scroll_decel; struct list_kinetic_scroll_settings kinetic_scroll_press; #endif diff --git a/apps/settings_list.c b/apps/settings_list.c index 958e61f07f..5d4e6e6cb7 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -2429,6 +2429,10 @@ const struct settings_list settings[] = { &list_kinetic_scroll_accel_default, "kinetic scroll accel", list_kinetic_load_from_cfg, list_kinetic_write_to_cfg, list_kinetic_is_default, list_kinetic_set_default), + CUSTOM_SETTING(0, kinetic_scroll_brake, -1, + &list_kinetic_scroll_brake_default, "kinetic scroll brake", + list_kinetic_load_from_cfg, list_kinetic_write_to_cfg, + list_kinetic_is_default, list_kinetic_set_default), CUSTOM_SETTING(0, kinetic_scroll_decel, -1, &list_kinetic_scroll_decel_default, "kinetic scroll decel", list_kinetic_load_from_cfg, list_kinetic_write_to_cfg, From 7e53e85cb23d8038a90c2f0670fdf4409de3ddcc Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Thu, 13 Aug 2026 09:35:11 -0400 Subject: [PATCH 078/408] Fix red in c93c7bfdcb -- Checkwps builds were failing Change-Id: I5d81c35d4d31036954e094d6430ef31daa34642b --- apps/settings_list.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/settings_list.c b/apps/settings_list.c index 5d4e6e6cb7..7731d37e2c 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -42,7 +42,9 @@ #include "open_plugin.h" #include "misc.h" #include "playback.h" +#ifndef __PCTOOL__ #include "fixedpoint.h" +#endif #ifdef HAVE_REMOTE_LCD #include "lcd-remote.h" #endif From 9641d54fee6f2fe4db50e8970ca3cb364d22640d Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Thu, 13 Aug 2026 09:59:52 -0400 Subject: [PATCH 079/408] Fix remaining red from c93c7bfdcb -- dbtool and warble builds Change-Id: I164aac856b821b9e7a96d807521912cb791e2152 --- apps/gui/list.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/gui/list.h b/apps/gui/list.h index 0e3a350ed7..faa5755b77 100644 --- a/apps/gui/list.h +++ b/apps/gui/list.h @@ -25,7 +25,9 @@ #include "action.h" #include "icon.h" #include "screen_access.h" +#ifndef __PCTOOL__ #include "skin_engine/skin_engine.h" +#endif #include "line.h" #define SCROLLBAR_WIDTH global_settings.scrollbar_width From 0db3308e43a1810a905251fe27a7bf5ed7e33f41 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Thu, 13 Aug 2026 12:35:55 -0400 Subject: [PATCH 080/408] FS#13981 - Updated Russian Translation (Ivan Romaniuk-Mikhailovsky) Change-Id: Ife14860048062922ac7b4eaffc42f9debe5a6ea2 --- apps/lang/russian.lang | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/lang/russian.lang b/apps/lang/russian.lang index 6d4e854d1a..c8da55dc91 100644 --- a/apps/lang/russian.lang +++ b/apps/lang/russian.lang @@ -32,6 +32,7 @@ # - Dmitry Prozorov # - Fedor Pomazkov # - Vladislav Voronichev +# - Sergey Puskov # - Ivan Romaniuk-Mikhailovsky id: LANG_SET_BOOL_YES @@ -736,10 +737,10 @@ *: "Peak Filter" - *: "Полосовой фильтр %d" + *: "Пиковый фильтр" - *: "Полосовой фильтр" + *: "Пиковый фильтр" From eded05f0cb1dade2791718ab0d16295328c5b166 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Thu, 13 Aug 2026 12:36:50 -0400 Subject: [PATCH 081/408] FS13980: Updated rbutil Italian Translation (Alessio Lenzi) Change-Id: Ic996de7db4a197858a7115627136d10a2f72be24 --- utils/rbutilqt/lang/rbutil_it.ts | 891 ++++++++++++++++--------------- 1 file changed, 462 insertions(+), 429 deletions(-) diff --git a/utils/rbutilqt/lang/rbutil_it.ts b/utils/rbutilqt/lang/rbutil_it.ts index 075037a0ab..9039dd870e 100644 --- a/utils/rbutilqt/lang/rbutil_it.ts +++ b/utils/rbutilqt/lang/rbutil_it.ts @@ -7,77 +7,77 @@ Backup - Backup + Backup <html><head/><body><p>This dialog will create a backup by archiving the contents of the Rockbox installation on the player into a zip file. This will include installed themes and settings stored below the .rockbox folder on the player.</p><p>The backup filename will be created based on the installed version. </p></body></html> - + <html><head/><body><p>Questa finestra crea una copia di sicurezza archiviando in un file ZIP il contenuto dell'installazione di Rockbox presente sul lettore. Saranno inclusi i temi installati e le impostazioni memorizzate nella cartella .rockbox del lettore.</p><p>Il nome del file di backup verrà generato in base alla versione installata.</p></body></html> Size: unknown - + Dimensione: sconosciuta Backup to: unknown - + Destinazione del backup: sconosciuta &Change - &Cambia + &Cambia &Backup - + &Backup &Cancel - + &Annulla Installation size: calculating ... - + Dimensione dell'installazione: calcolo in corso... Select Backup Filename - Scegliere un nome per il file di backup + Scegli il nome del file di backup Installation size: %L1 %2 - + Dimensione dell'installazione: %L1 %2 File exists - + Il file esiste già The selected backup file already exists. Overwrite? - + Il file di backup selezionato esiste già. Sovrascriverlo? Starting backup ... - + Avvio del backup... Backup successful. - + Backup completato. Backup failed! - Backup fallito! + Backup non riuscito. @@ -85,48 +85,48 @@ Bootloader installation requires you to provide a copy of the original Sandisk firmware (bin file). This firmware file will be patched and then installed to your player along with the rockbox bootloader. You need to download this file yourself due to legal reasons. Please browse the <a href='http://forums.sandisk.com/sansa/'>Sansa Forums</a> or refer to the <a href='https://www.rockbox.org/manual.shtml'>manual</a> and the <a href='https://www.rockbox.org/wiki/SansaAMS'>SansaAMS</a> wiki page on how to obtain this file.<br/><b>Note:</b> This file is not present on your player and will disappear automatically after installing it.<br/><br/>Press Ok to continue and browse your computer for the firmware file. - + L'installazione del bootloader richiede una copia del firmware originale Sandisk (file BIN). Il firmware verrà modificato e quindi installato sul tuo lettore insieme al bootloader Rockbox. Per motivi legali, devi scaricare manualmente questo file. Consulta i <a href='http://forums.sandisk.com/sansa/'>forum Sansa</a> oppure il <a href='https://www.rockbox.org/manual.shtml'>manuale</a> e la pagina wiki <a href='https://www.rockbox.org/wiki/SansaAMS'>SansaAMS</a> per sapere come ottenerlo.<br/><b>Nota:</b> Il file non è presente sul lettore e verrà eliminato automaticamente dopo l'installazione.<br/><br/>Premi OK per continuare e selezionare il file del firmware sul computer. Downloading bootloader file - + Download del file del bootloader in corso Could not load %1 - + Impossibile caricare %1 No room to insert bootloader, try another firmware version - + Nel firmware non c’è spazio sufficiente per inserire il bootloader. Prova un’altra versione del firmware Patching Firmware... - + Modifica del firmware in corso... Could not open %1 for writing - + Impossibile aprire %1 in scrittura Could not write firmware file - + Impossibile scrivere il file del firmware Success: modified firmware file created - Firmware modificato creato con successo + Firmware modificato creato con successo To uninstall, perform a normal upgrade with an unmodified original firmware - + Per disinstallarlo, esegui un normale aggiornamento con un firmware originale non modificato @@ -134,47 +134,47 @@ Bootloader installation requires you to provide the correct verrsion of the original firmware file. This file will be patched with the Rockbox bootloader and installed to your player. You need to download this file yourself due to legal reasons. Please refer to the <a href='https://www.rockbox.org/wiki/'>rockbox wiki</a> pages on how to obtain this file.<br/>Press Ok to continue and browse your computer for the firmware file. - + Per installare il bootloader devi fornire la versione corretta del firmware originale. Il file verrà modificato con il bootloader Rockbox e installato sul tuo lettore. Per motivi legali, devi scaricarlo manualmente. Consulta le pagine della <a href='https://www.rockbox.org/wiki/'>wiki di Rockbox</a> per sapere come ottenerlo.<br/>Premi OK per continuare e selezionare il file del firmware sul computer. Could not read original firmware file - + Impossibile leggere il file del firmware originale Downloading bootloader file - + Download del file del bootloader in corso Patching file... - + Modifica del file in corso... Patching the original firmware failed - + La modifica del firmware originale non è riuscita Succesfully patched firmware file - + File del firmware modificato correttamente Bootloader successful installed - Bootloader installato con successo + Bootloader installato con successo Patched bootloader could not be installed - + Impossibile installare il bootloader modificato To uninstall, perform a normal upgrade with an unmodified original firmware. - + Per disinstallarlo, esegui un normale aggiornamento con un firmware originale non modificato. @@ -232,42 +232,42 @@ Zip file format detected - + Rilevato il formato ZIP CAB file format detected - + Rilevato il formato CAB Extracting firmware %1 from archive - + Estrazione del firmware %1 dall'archivio Error extracting firmware from archive - + Errore durante l'estrazione del firmware dall'archivio Could not find firmware in archive - + Impossibile trovare il firmware nell'archivio Waiting for system to remount player - + In attesa che il sistema renda nuovamente disponibile il lettore Player remounted - + Il lettore è nuovamente disponibile Timeout on remount - + Il lettore non è tornato disponibile entro il tempo previsto @@ -275,77 +275,77 @@ Bootloader installation requires you to provide a firmware file of the original firmware (HXF file). You need to download this file yourself due to legal reasons. Please refer to the <a href='https://www.rockbox.org/manual.shtml'>manual</a> and the <a href='https://www.rockbox.org/wiki/OndaVX747#Download_and_extract_a_recent_ve'>OndaVX747</a> wiki page on how to obtain this file.<br/>Press Ok to continue and browse your computer for the firmware file. - + L'installazione del bootloader richiede un file del firmware originale (file HXF). Per motivi legali, devi scaricarlo manualmente. Consulta il <a href='https://www.rockbox.org/manual.shtml'>manuale</a> e la pagina wiki <a href='https://www.rockbox.org/wiki/OndaVX747#Download_and_extract_a_recent_ve'>OndaVX747</a> per sapere come ottenerlo.<br/>Premi OK per continuare e selezionare il file del firmware sul computer. Downloading bootloader file - + Download del file del bootloader in corso Could not open firmware file - + Impossibile aprire il file del firmware Could not open bootloader file - + Impossibile aprire il file del bootloader Could not allocate memory - + Impossibile allocare la memoria Could not load firmware file - + Impossibile caricare il file del firmware File is not a valid ChinaChip firmware - + Il file non è un firmware ChinaChip valido Could not find ccpmp.bin in input file - + Impossibile trovare ccpmp.bin nel file di origine Could not open backup file for ccpmp.bin - + Impossibile aprire il file di backup per ccpmp.bin Could not write backup file for ccpmp.bin - + Impossibile scrivere il file di backup per ccpmp.bin Could not load bootloader file - + Impossibile caricare il file del bootloader Could not get current time - + Impossibile ottenere l'ora corrente Could not open output file - + Impossibile aprire il file di destinazione Could not write output file - + Impossibile scrivere il file di destinazione Unexpected error from chinachippatcher - + Errore imprevisto restituito da chinachippatcher @@ -368,7 +368,7 @@ A firmware file is already present on player - + Un file del firmware è già presente sul lettore @@ -378,7 +378,7 @@ Copying modified firmware file failed - + Impossibile copiare il file del firmware modificato @@ -411,7 +411,7 @@ Bootloader installation requires you to provide a firmware file of the original firmware (hex file). You need to download this file yourself due to legal reasons. Please refer to the <a href='https://www.rockbox.org/manual.shtml'>manual</a> and the <a href='https://www.rockbox.org/wiki/IriverBoot#Download_and_extract_a_recent_ve'>IriverBoot</a> wiki page on how to obtain this file.<br/>Press Ok to continue and browse your computer for the firmware file. - L'installazione del bootloader richiede di fornire il file del firmware originale(hex file). Occorre scaricarlo a parte a causa di problemi legali. Fate riferimento al <ahref='https://www.rockbox.org/manual.shtml'>manuale</a> e al <ahref='https://www.rockbox.org/wiki/IriverBoot#Download_and_extract_a_recent_ve'>wiki IriverBoot</a>per ottenere il file.<br/>Premere Ok per continuare e sfogliare le cartelle per cercare il file del firmware. + L'installazione del bootloader richiede un file del firmware originale (file HEX). Per motivi legali, devi scaricarlo manualmente. Consulta il <a href='https://www.rockbox.org/manual.shtml'>manuale</a> e la pagina wiki <a href='https://www.rockbox.org/wiki/IriverBoot#Download_and_extract_a_recent_ve'>IriverBoot</a> per sapere come ottenerlo.<br/>Premi OK per continuare e selezionare il file del firmware sul computer. @@ -436,7 +436,7 @@ Firmware file doesn't match selected player. - Il firmware non è compatibile col player selezionato. + Il firmware non è compatibile con il lettore selezionato. @@ -516,7 +516,7 @@ A firmware file is already present on player - + Un file del firmware è già presente sul lettore @@ -526,12 +526,12 @@ Copying modified firmware file failed - + Impossibile copiare il file del firmware modificato Uninstallation not possible, only installation info removed - + Impossibile disinstallare: sono state rimosse solo le informazioni sull'installazione @@ -584,47 +584,47 @@ Bootloader installation requires you to provide a copy of the original Sandisk firmware (firmware.sb file). This file will be patched with the Rockbox bootloader and installed to your player. You need to download this file yourself due to legal reasons. Please browse the <a href='http://forums.sandisk.com/sansa/'>Sansa Forums</a> or refer to the <a href='https://www.rockbox.org/manual.shtml'>manual</a> and <a href= 'https://www.rockbox.org/wiki/SansaFuzePlus'>SansaFuzePlus</a> wiki page on how to obtain this file.<br/>Press Ok to continue and browse your computer for the firmware file. - + L'installazione del bootloader richiede una copia del firmware originale Sandisk (file firmware.sb). Il file verrà modificato con il bootloader Rockbox e installato sul tuo lettore. Per motivi legali, devi scaricarlo manualmente. Consulta i <a href='http://forums.sandisk.com/sansa/'>forum Sansa</a> oppure il <a href='https://www.rockbox.org/manual.shtml'>manuale</a> e la pagina wiki <a href= 'https://www.rockbox.org/wiki/SansaFuzePlus'>SansaFuzePlus</a> per sapere come ottenerlo.<br/>Premi OK per continuare e selezionare il file del firmware sul computer. Could not read original firmware file - + Impossibile leggere il file del firmware originale Downloading bootloader file - + Download del file del bootloader in corso Patching file... - + Modifica del file in corso... Patching the original firmware failed - + La modifica del firmware originale non è riuscita Succesfully patched firmware file - + File del firmware modificato correttamente Bootloader successful installed - Bootloader installato con successo + Bootloader installato con successo Patched bootloader could not be installed - + Impossibile installare il bootloader modificato To uninstall, perform a normal upgrade with an unmodified original firmware. - + Per disinstallarlo, esegui un normale aggiornamento con un firmware originale non modificato. @@ -659,7 +659,8 @@ Warning: This is a MacPod, Rockbox only runs on WinPods. See https://www.rockbox.org/wiki/IpodConversionToFAT32 - + Attenzione: questo è un MacPod; Rockbox funziona solo sui WinPod. +Consulta https://www.rockbox.org/wiki/IpodConversionToFAT32 @@ -680,12 +681,12 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 Bootloader Installation complete. - + Installazione del bootloader completata. Writing log aborted - + Scrittura del registro interrotta @@ -710,17 +711,17 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 Error: could not retrieve device name - + Errore: impossibile recuperare il nome del dispositivo Error: no mountpoint specified! - + Errore: nessun punto di montaggio specificato! Could not open Ipod: permission denied - + Impossibile aprire l'iPod: accesso negato @@ -753,7 +754,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 A firmware file is already present on player - + Un file del firmware è già presente sul lettore @@ -764,7 +765,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 Copying modified firmware file failed - + Impossibile copiare il file del firmware modificato @@ -797,77 +798,77 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 Bootloader installation requires you to provide a firmware file of the original firmware (bin file). You need to download this file yourself due to legal reasons. Please refer to the <a href='https://www.rockbox.org/manual.shtml'>manual</a> and the <a href='https://www.rockbox.org/wiki/MPIOHD200Port'>MPIOHD200Port</a> wiki page on how to obtain this file.<br/>Press Ok to continue and browse your computer for the firmware file. - + L'installazione del bootloader richiede un file del firmware originale (file BIN). Per motivi legali, devi scaricarlo manualmente. Consulta il <a href='https://www.rockbox.org/manual.shtml'>manuale</a> e la pagina wiki <a href='https://www.rockbox.org/wiki/MPIOHD200Port'>MPIOHD200Port</a> per sapere come ottenerlo.<br/>Premi OK per continuare e selezionare il file del firmware sul computer. Downloading bootloader file - + Download del file del bootloader in corso Could not open the original firmware. - + Impossibile aprire il firmware originale. Could not read the original firmware. - + Impossibile leggere il firmware originale. Loaded firmware file does not look like MPIO original firmware file. - + Il file caricato non sembra essere un firmware originale MPIO. Could not open output file. - + Impossibile aprire il file di destinazione. Could not write output file. - + Impossibile scrivere il file di destinazione. Unknown error number: %1 - + Numero di errore sconosciuto: %1 Could not open downloaded bootloader. - + Impossibile aprire il bootloader scaricato. Place for bootloader in OF file not empty. - + Lo spazio riservato al bootloader nel file del firmware originale (OF) non è vuoto. Could not read the downloaded bootloader. - + Impossibile leggere il bootloader scaricato. Bootloader checksum error. - + Errore di checksum del bootloader. Patching original firmware failed: %1 - + La modifica del firmware originale non è riuscita: %1 Success: modified firmware file created - Firmware modificato creato con successo + Firmware modificato creato con successo To uninstall, perform a normal upgrade with an unmodified original firmware - + Per disinstallarlo, esegui un normale aggiornamento con un firmware originale non modificato @@ -875,149 +876,161 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 Could not find mounted iPod. - + Impossibile trovare l'iPod tra i dispositivi disponibili. Downloading bootloader file... - + Download del file del bootloader in corso... Could not make DFU image. - + Impossibile creare l'immagine DFU. Ejecting iPod... - + Rimozione sicura dell'iPod in corso... Action required: Please make sure no programs are accessing files on the device. If ejecting still fails please use your computers eject functionality. - + Azione richiesta: + +Assicurati che nessun programma stia accedendo ai file sul dispositivo. Se la rimozione sicura continua a non riuscire, usa la funzione di rimozione sicura del computer. Device successfully ejected. - + Il dispositivo può essere scollegato in sicurezza. Action required: Quit iTunes application. - + Azione richiesta: + +Chiudi iTunes. iTunes closed. - + iTunes chiuso. Could not suspend iTunesHelper. Stop it using the Task Manager, and try again. - + Impossibile sospendere iTunesHelper. Termina il processo da Gestione attività e riprova. Waiting for HDD spin-down... - + In attesa che il disco rigido si arresti... Waiting for DFU mode... - + In attesa della modalità DFU... Action required: Press and hold SELECT+MENU buttons, after about 12 seconds a new action will require you to release the buttons, DO IT QUICKLY, otherwise the process could fail. - + Azione richiesta: + +Tieni premuti i pulsanti SELECT+MENU. Tra circa 12 secondi ti verrà chiesto di rilasciarli: FALLO SUBITO, altrimenti il processo potrebbe non riuscire. DFU mode detected. - + Rilevata la modalità DFU. Action required: Release SELECT+MENU buttons and wait... - + Azione richiesta: + +Rilascia i pulsanti SELECT+MENU e attendi... Device is not in DFU mode. It seems that the previous required action failed, please try again. - + Il dispositivo non è in modalità DFU. Sembra che l'azione richiesta in precedenza non sia riuscita; riprova. Transfering DFU image... - + Trasferimento dell'immagine DFU in corso... No valid DFU USB driver found. Install iTunes (or the Apple Device Driver) and try again. - + Non è stato trovato alcun driver USB DFU valido. + +Installa iTunes (o il driver del dispositivo Apple) e riprova. Could not transfer DFU image. - + Impossibile trasferire l'immagine DFU. DFU transfer completed. - + Trasferimento DFU completato. Restarting iPod, waiting for remount... - + Riavvio dell'iPod; in attesa che torni disponibile... Action required: Could not remount the device, try to do it manually. If the iPod didn't restart, force a reset by pressing SELECT+MENU buttons for about 5 seconds. If the problem could not be solved then click 'Abort' to cancel. - + Azione richiesta: + +Il dispositivo non è tornato disponibile; prova a ricollegarlo manualmente. Se l'iPod non si è riavviato, forza il riavvio tenendo premuti i pulsanti SELECT+MENU per circa 5 secondi. Se il problema persiste, fai clic su «Interrompi» per annullare. Device remounted. - + Il dispositivo è nuovamente disponibile. Bootloader successfully installed. - + Bootloader installato correttamente. Bootloader successfully uninstalled. - + Bootloader disinstallato correttamente. Could not resume iTunesHelper. - + Impossibile riattivare iTunesHelper. Install aborted by user. - + Installazione interrotta dall'utente. Uninstall aborted by user. - + Disinstallazione interrotta dall'utente. @@ -1093,12 +1106,12 @@ Vedi https://www.rockbox.org/wiki/SansaE200Install Bootloader Installation complete. - + Installazione del bootloader completata. Writing log aborted - + Scrittura del registro interrotta @@ -1118,7 +1131,7 @@ Vedi https://www.rockbox.org/wiki/SansaE200Install Error: could not retrieve device name - + Errore: impossibile recuperare il nome del dispositivo @@ -1146,53 +1159,53 @@ Vedi https://www.rockbox.org/wiki/SansaE200Install Bootloader installation requires you to provide a firmware file of the original firmware (bin file). You need to download this file yourself due to legal reasons. Please refer to the <a href='https://www.rockbox.org/manual.shtml'>manual</a> and the <a href='https://www.rockbox.org/wiki/CowonD2Info'>CowonD2Info</a> wiki page on how to obtain the file.<br/>Press Ok to continue and browse your computer for the firmware file. - + L'installazione del bootloader richiede un file del firmware originale (file BIN). Per motivi legali, devi scaricarlo manualmente. Consulta il <a href='https://www.rockbox.org/manual.shtml'>manuale</a> e la pagina wiki <a href='https://www.rockbox.org/wiki/CowonD2Info'>CowonD2Info</a> per sapere come ottenerlo.<br/>Premi OK per continuare e selezionare il file del firmware sul computer. Downloading bootloader file - + Download del file del bootloader in corso Could not load %1 - + Impossibile caricare %1 Unknown OF file used: %1 - + File del firmware originale (OF) non riconosciuto: %1 Patching Firmware... - + Modifica del firmware in corso... Could not patch firmware - + Impossibile modificare il firmware Could not open %1 for writing - + Impossibile aprire %1 in scrittura Could not write firmware file - + Impossibile scrivere il file del firmware Success: modified firmware file created - Firmware modificato creato con successo + Firmware modificato creato con successo To uninstall, perform a normal upgrade with an unmodified original firmware - + Per disinstallarlo, esegui un normale aggiornamento con un firmware originale non modificato @@ -1200,17 +1213,17 @@ Vedi https://www.rockbox.org/wiki/SansaE200Install Changelog - + Changelog Show on startup - + Mostra all'avvio &Ok - &Ok + &Ok @@ -1223,27 +1236,27 @@ Vedi https://www.rockbox.org/wiki/SansaE200Install No mountpoint given - Nessun mountpoint scelto + Nessun punto di montaggio selezionato Mountpoint does not exist - Mountpoint inesistente + Il punto di montaggio non esiste Mountpoint is not a directory. - Il Mountpoint non è una cartella. + Il punto di montaggio non è una cartella. Mountpoint is not writeable - Il Mountpoint non è scrivibile + Il punto di montaggio non è scrivibile No player selected - Nessun player selezionato + Nessun lettore selezionato @@ -1268,24 +1281,24 @@ Vedi https://www.rockbox.org/wiki/SansaE200Install Showing disabled targets - + Visualizzazione dei dispositivi disabilitati You just enabled showing targets that are marked disabled. Disabled targets are not recommended to end users. Please use this option only if you know what you are doing. - + Hai attivato la visualizzazione dei dispositivi contrassegnati come disabilitati. L'uso di questi dispositivi non è consigliato agli utenti finali. Usa questa opzione solo se sai cosa stai facendo. TTS error - + Errore TTS The selected TTS failed to initialize. You can't use this TTS. - + L'inizializzazione del TTS selezionato non è riuscita. Non puoi utilizzarlo. @@ -1302,61 +1315,63 @@ Vedi https://www.rockbox.org/wiki/SansaE200Install Proxy Detection - + Rilevamento del proxy The System Proxy settings are invalid! Rockbox Utility can't work with this proxy settings. Make sure the system proxy is set correctly. Note that "proxy auto-config (PAC)" scripts are not supported by Rockbox Utility. If your system uses this you need to use manual proxy settings. - + Le impostazioni del proxy di sistema non sono valide! +Rockbox Utility non può funzionare con queste impostazioni. Assicurati che il proxy di sistema sia configurato correttamente. Gli script di configurazione automatica del proxy (PAC) non sono supportati da Rockbox Utility. Se il sistema ne utilizza uno, devi configurare manualmente il proxy. Set Cache Path - + Imposta il percorso della cache %1 (%2 GiB of %3 GiB free) - + %1 (%2 GiB liberi su %3 GiB) Multiple devices have been detected. Please disconnect all players but one and try again. - + Sono stati rilevati più dispositivi. Scollega tutti i lettori tranne uno e riprova. Detected devices: - + Dispositivi rilevati: (unknown) - + (sconosciuto) %1 at %2 - + %1 in %2 Note: detecting connected devices might be ambiguous. You might have less devices connected than listed. In this case it might not be possible to detect your player unambiguously. - + Nota: il rilevamento dei dispositivi collegati può dare risultati ambigui. Potrebbero essere elencati più dispositivi di quelli realmente collegati. In tal caso potrebbe non essere possibile identificare il lettore in modo univoco. Device Detection - + Rilevamento del dispositivo %1 "MacPod" found! Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. - + Rilevato %1 «MacPod»! +Per funzionare, Rockbox richiede un iPod con file system FAT (il cosiddetto «WinPod»). Fatal error @@ -1367,52 +1382,58 @@ Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. Detected an unsupported player: %1 Sorry, Rockbox doesn't run on your player. - + Rilevato un lettore non supportato: +%1 +Rockbox non è compatibile con questo lettore. TTS configuration invalid - + Configurazione TTS non valida TTS configuration invalid. Please configure TTS engine. - + Configurazione del TTS non valida. +Configura il motore TTS. Could not start TTS engine. - + Impossibile avviare il motore TTS. Could not start TTS engine. - + Impossibile avviare il motore TTS. + Please configure TTS engine. - + +Configura il motore TTS. Rockbox Utility Voice Test - + Test vocale di Rockbox Utility Could not voice test string. - + Impossibile pronunciare la frase di prova. Could not voice test string. - + Impossibile pronunciare la frase di prova. + @@ -1423,30 +1444,34 @@ Please configure TTS engine. %1 in MTP mode found! You need to change your player to MSC mode for installation. - + Rilevato %1 in modalità MTP! +Per l'installazione devi impostare il lettore in modalità MSC. Could not detect a Mountpoint. Select your Mountpoint manually. - Impossibile individuare un Mountpoint.Selezionare un Mountpoint manualmente. + Impossibile rilevare un punto di montaggio. +Seleziona manualmente il punto di montaggio. Could not detect a device. Select your device and Mountpoint manually. - Impossibile individuare il dispositivo.Selezionare il dispositivo e il Mountpoint manualmente. + Impossibile rilevare il dispositivo. +Seleziona manualmente il dispositivo e il punto di montaggio. The player contains an incompatible filesystem. Make sure you selected the correct mountpoint and the player is set up to use a filesystem compatible with Rockbox. - + Il lettore utilizza un file system incompatibile. +Assicurati di aver selezionato il punto di montaggio corretto e che il lettore utilizzi un file system compatibile con Rockbox. An unknown error occured during player detection. - + Si è verificato un errore sconosciuto durante il rilevamento del lettore. @@ -1499,17 +1524,17 @@ Make sure you selected the correct mountpoint and the player is set up to use a &Select your audio player - &Seleziona il tuo player audio + &Seleziona il lettore audio &Refresh - &Aggiorna + &Aggiorna Show disabled targets - + Mostra i dispositivi disabilitati @@ -1564,7 +1589,7 @@ Make sure you selected the correct mountpoint and the player is set up to use a Show - + Mostra @@ -1618,7 +1643,7 @@ Make sure you selected the correct mountpoint and the player is set up to use a &TTS && Encoder - &TTS &&Encoder + &TTS && codificatore @@ -1628,12 +1653,12 @@ Make sure you selected the correct mountpoint and the player is set up to use a &Select TTS Engine - &Scegli engine TTS + &Scegli motore TTS Configure TTS Engine - Configura engine TTS + Configura motore TTS @@ -1649,17 +1674,17 @@ Make sure you selected the correct mountpoint and the player is set up to use a Test TTS - + Test TTS &Use string corrections for TTS - + &Usa le correzioni di pronuncia per il TTS Encoder Engine - Engine encoder + Motore di codifica @@ -1669,7 +1694,7 @@ Make sure you selected the correct mountpoint and the player is set up to use a encoder name - nome encoder + Nome del codificatore @@ -1679,7 +1704,7 @@ Make sure you selected the correct mountpoint and the player is set up to use a &Cancel - &Cancel + &Annulla @@ -1696,22 +1721,22 @@ Make sure you selected the correct mountpoint and the player is set up to use a Create Voice File - Crea file voce + Crea file vocale Select the Language you want to generate a voicefile for: - seleziona la lingua per la quale vuoi generare un file voce: + Seleziona la lingua per la quale vuoi generare un file vocale: TTS: - + TTS: Silence threshold - + Soglia del silenzio @@ -1749,18 +1774,18 @@ Make sure you selected the correct mountpoint and the player is set up to use a TTS error - + Errore TTS The selected TTS failed to initialize. You can't use this TTS. - + L'inizializzazione del TTS selezionato non è riuscita. Non puoi utilizzarlo. Engine: <b>%1</b> - + Motore: <b>%1</b> @@ -1793,7 +1818,7 @@ Make sure you selected the correct mountpoint and the player is set up to use a Select executable - + Seleziona il file eseguibile @@ -1801,12 +1826,12 @@ Make sure you selected the correct mountpoint and the player is set up to use a Path to Encoder: - Percorso dell'Encoder: + Percorso del codificatore: Encoder options: - Opzioni per l'Encoder: + Opzioni del codificatore: @@ -1815,22 +1840,22 @@ Make sure you selected the correct mountpoint and the player is set up to use a LAME - + LAME Volume - + Volume Quality - + Qualità Could not find libmp3lame! - + Impossibile trovare la libreria libmp3lame! @@ -1838,22 +1863,22 @@ Make sure you selected the correct mountpoint and the player is set up to use a Volume: - Volume: + Volume: Quality: - Qualità: + Qualità: Complexity: - Complessità: + Complessità: Use Narrowband: - Usa Narrowband: + Usa la banda stretta: @@ -1862,18 +1887,18 @@ Make sure you selected the correct mountpoint and the player is set up to use a File - File + File Version - Versione + Versione Loading, please wait ... - + Caricamento in corso, attendi... @@ -1881,17 +1906,17 @@ Make sure you selected the correct mountpoint and the player is set up to use a Info - info + Informazioni Currently installed packages.<br/><b>Note:</b> if you manually installed packages this might not be correct! - Pacchetti correntemente installati.<br/><b>Nota:</b>se si sono installati dei pacchetti manualmente potrebbero non essere visti! + Pacchetti attualmente installati.<br/><b>Nota:</b> se hai installato manualmente alcuni pacchetti, queste informazioni potrebbero non essere corrette. Package - + Pacchetto @@ -1904,7 +1929,7 @@ Make sure you selected the correct mountpoint and the player is set up to use a Generate for files - + Crea file vocali .talk per i file @@ -1914,12 +1939,12 @@ Make sure you selected the correct mountpoint and the player is set up to use a Generate for folders - + Crea file vocali .talk per le cartelle Recurse into folders - + Includi le sottocartelle @@ -1929,17 +1954,17 @@ Make sure you selected the correct mountpoint and the player is set up to use a Ignore files - + Ignora i file Skip existing - + Salta i file vocali .talk esistenti Select folders for Talkfile generation (Ctrl for multiselect) - + Seleziona le cartelle in cui creare i file vocali .talk (tieni premuto Ctrl per selezioni multiple) @@ -1949,7 +1974,7 @@ Make sure you selected the correct mountpoint and the player is set up to use a Strip Extensions - + Non pronunciare le estensioni @@ -1959,7 +1984,7 @@ Make sure you selected the correct mountpoint and the player is set up to use a &Cancel - &Cancella + &Annulla @@ -1967,22 +1992,22 @@ Make sure you selected the correct mountpoint and the player is set up to use a Empty selection - + Selezione vuota No files or folders selected. Please select files or folders first. - + Non hai selezionato file o cartelle. Selezionali prima di continuare. TTS error - + Errore TTS The selected TTS failed to initialize. You can't use this TTS. - + L'inizializzazione del TTS selezionato non è riuscita. Non puoi utilizzarlo. @@ -2048,12 +2073,12 @@ Make sure you selected the correct mountpoint and the player is set up to use a Creating output path failed - + Impossibile creare il percorso di destinazione Error during CAB operation - + Errore durante l'estrazione dall'archivio CAB @@ -2061,27 +2086,27 @@ Make sure you selected the correct mountpoint and the player is set up to use a Stable (Retired) - + Stabile (non più mantenuto) Unusable - + Non utilizzabile Unstable - + Instabile Stable - + Stabile Unknown - + Sconosciuto @@ -2108,7 +2133,7 @@ Make sure you selected the correct mountpoint and the player is set up to use a Save Log - + Salva il registro @@ -2126,12 +2151,12 @@ Make sure you selected the correct mountpoint and the player is set up to use a &Ok - + &OK Save system trace log - + Salva il registro diagnostico del sistema @@ -2141,67 +2166,67 @@ Make sure you selected the correct mountpoint and the player is set up to use a LTR This string is used to indicate the writing direction. Translate it to "RTL" (without quotes) for RTL languages. Anything else will get treated as LTR language. - + LTR (unknown vendor name) - + (produttore sconosciuto) (unknown product name) - + (modello sconosciuto) Before Bootloader installation begins, Please check the following: - + Prima di iniziare l'installazione del bootloader, verifica quanto segue: <li>Ensure your SD card is formatted as FAT. exFAT is <i>not</i> supported. You can reformat using the Original Firmware on your player if need be. It is located under (System Settings --> Reset --> Format TF Card).</li><li>Please use a quality SD card from a reputable source. The SD cards that come bundled with players are often of substandard quality and may cause issues.</li> - + <li>Assicurati che la scheda SD sia formattata con il file system FAT. exFAT <i>non</i> è supportato. Se necessario, puoi riformattarla tramite il firmware originale del lettore, usando la voce System Settings --> Reset --> Format TF Card.</li><li>Usa una scheda SD di qualità e di una marca affidabile. Le schede SD fornite con i lettori sono spesso di scarsa qualità e possono causare problemi.</li> Bootloader installation is almost complete. Installation <b>requires</b> you to perform the following steps manually: - l'installazione del bootloader è quasi completa. Per continuare <b>occorre</b> che tu faccia le seguenti operazioni manualmente: + L'installazione del bootloader è quasi completa. Per completarla, <b>devi</b> eseguire manualmente le seguenti operazioni: <li>Safely remove your player.</li> - <li>Rimuovi il tuo player in maniera sicura.</li> + <li>Esegui la rimozione sicura del lettore.</li> <li>Reboot your player into the original firmware.</li><li>Perform a firmware upgrade using the update functionality of the original firmware. Please refer to your player's manual on details.<br/><b>Important:</b> updating the firmware is a critical process that must not be interrupted. <b>Make sure the player is charged before starting the firmware update process.</b></li><li>After the firmware has been updated reboot your player.</li> - + <li>Riavvia il lettore con il firmware originale.</li><li>Aggiorna il firmware usando la funzione di aggiornamento del firmware originale. Per i dettagli, consulta il manuale del lettore.<br/><b>Importante:</b> l'aggiornamento del firmware è un processo critico che non deve essere interrotto. <b>Assicurati che il lettore sia carico prima di iniziare.</b></li><li>Al termine dell'aggiornamento, riavvia il lettore.</li> <li>Remove any previously inserted microSD card</li> - + <li>Rimuovi l'eventuale scheda microSD inserita</li> <li>Disconnect your player. The player will reboot and perform an update of the original firmware. Please refer to your players manual on details.<br/><b>Important:</b> updating the firmware is a critical process that must not be interrupted. <b>Make sure the player is charged before disconnecting the player.</b></li><li>After the firmware has been updated reboot your player.</li> - + <li>Scollega il lettore. Il lettore si riavvierà e aggiornerà il firmware originale. Per i dettagli, consulta il manuale del lettore.<br/><b>Importante:</b> l'aggiornamento del firmware è un processo critico che non deve essere interrotto. <b>Assicurati che il lettore sia carico prima di scollegarlo.</b></li><li>Al termine dell'aggiornamento, riavvia il lettore.</li> <li>Turn the player off</li><li>Insert the charger</li> - <li>Spegni il player</li><li>Inserisci il caricabatterie</li> + <li>Spegni il lettore</li><li>Collega il caricabatterie</li> <li>Unplug USB and power adaptors</li><li>Hold <i>Power</i> to turn the player off</li><li>Toggle the battery switch on the player</li><li>Hold <i>Power</i> to boot into Rockbox</li> - <li>Stacca il cavo USB</li><li>Tieni premuto il tasto<i>Power</i>per spegnere il player</li><li>Sposta l'interruttore della batteria sul player</li><li>Tieni premuto <i>Power</i> per avviare Rockbox</li> + <li>Scollega il cavo USB e l'alimentatore</li><li>Tieni premuto <i>Power</i> per spegnere il lettore</li><li>Sposta l'interruttore della batteria sul lettore</li><li>Tieni premuto <i>Power</i> per avviare Rockbox</li> <p><b>Note:</b> You can safely install other parts first, but the above steps are <b>required</b> to finish the installation!</p> - <p><b>Note:</b>Puoi installare in tutta sicurezza prima le altre componenti, ma i passi precedenti <b>sono indispensabili</b>per concludere l'installazione!</p> + <p><b>Nota:</b> puoi prima installare in sicurezza gli altri componenti, ma per completare l'installazione <b>devi</b> eseguire le operazioni indicate sopra.</p> @@ -2209,7 +2234,7 @@ Make sure you selected the correct mountpoint and the player is set up to use a ZIP/UNZIP API error %1 - + Errore dell'API ZIP/UNZIP: %1 @@ -2222,7 +2247,7 @@ Make sure you selected the correct mountpoint and the player is set up to use a Libraries used - + Librerie utilizzate @@ -2248,22 +2273,22 @@ Make sure you selected the correct mountpoint and the player is set up to use a Wine detected! - + Wine rilevato! It seems you are trying to run this program under Wine. Please don't do this, running under Wine will fail. Use the native Linux binary instead. - + Sembra che tu stia tentando di eseguire questo programma con Wine. Non farlo: Rockbox Utility non funziona con Wine. Usa invece il programma nativo per Linux. Downloading build information, please wait ... - + Download delle informazioni sulle build in corso, attendi... Certificate error - + Errore del certificato @@ -2275,23 +2300,31 @@ Valid since: %4 Valid until: %5 Temporarily trust certificate? - + %1 + +Emittente: %2 +Soggetto: %3 +Valido da: %4 +Valido fino a: %5 + +Vuoi considerare temporaneamente attendibile questo certificato? Can't get version information! - + Impossibile ottenere informazioni sulla versione! Can't get version information. Network error: %1. Please check your network and proxy settings. - + Impossibile ottenere informazioni sulla versione. +Errore di rete: %1. Controlla le impostazioni di rete e del proxy. Download build information finished. - + Download delle informazioni sulle build completato. Confirm Installation @@ -2327,17 +2360,17 @@ Network error: %1. Please check your network and proxy settings. No uninstall method for this target known. - + Nessun metodo di disinstallazione noto per questo dispositivo. New version of Rockbox Utility available. - + Disponibile una nuova versione di Rockbox Utility. Rockbox Utility is up to date. - + Rockbox Utility è aggiornata. @@ -2347,22 +2380,22 @@ Network error: %1. Please check your network and proxy settings. Rockbox Utility can not uninstall the bootloader on your player. Please perform a firmware update using your player vendors firmware update process. - + Rockbox Utility non può disinstallare il bootloader sul tuo lettore. Aggiorna il firmware seguendo la procedura fornita dal produttore del lettore. Important: make sure to boot your player into the original firmware before using the vendors firmware update process. - + Importante: assicurati di avviare il lettore con il firmware originale prima di seguire la procedura di aggiornamento del produttore. No Rockbox bootloader found. - + Non è stato trovato alcun bootloader Rockbox. Do you really want to install Rockbox Utility to your player? After installation you can run it from the players hard drive. - Vuoi installare la Rockbox utility nel tuo player ? Dopo l'installazione potrai avviarla dal disco del tuo player. + Vuoi davvero installare Rockbox Utility sul lettore? Dopo l'installazione potrai avviarla dalla memoria del lettore. @@ -2397,37 +2430,37 @@ Network error: %1. Please check your network and proxy settings. Checking for update ... - + Verifica dell'aggiornamento in corso... Rockbox Utility Update available - + Disponibile un aggiornamento di Rockbox Utility <b>New Rockbox Utility version available.</b><br><br>You are currently using version %1. Get version %2 at <a href='%3'>%3</a> - + <b>Disponibile una nuova versione di Rockbox Utility.</b><br><br>Stai utilizzando la versione %1. Scarica la versione %2 da <a href='%3'>%3</a> Device ejected - + Rimozione sicura completata Device successfully ejected. You may now disconnect the player from the PC. - + Il dispositivo può essere scollegato in sicurezza. Ora puoi scollegare il lettore dal computer. Ejecting failed - + Rimozione sicura non riuscita Ejecting the device failed. Please make sure no programs are accessing files on the device. If ejecting still fails please use your computers eject funtionality. - + La rimozione sicura del dispositivo non è riuscita. Assicurati che nessun programma stia accedendo ai file sul dispositivo. Se il problema persiste, usa la funzione di rimozione sicura del computer. @@ -2480,23 +2513,23 @@ Network error: %1. Please check your network and proxy settings. Install Talk files - Installa file Talk + Crea file vocali .talk <b>Create Talk Files</b><br/>Talkfiles are needed to let Rockbox speak File and Foldernames - + <b>Crea file vocali .talk</b><br/>I file vocali .talk consentono a Rockbox di pronunciare i nomi dei file e delle cartelle Create Voice files - Crea file Voice + Crea file vocale <b>Create Voice file</b><br/>Voice files are needed to make Rockbox speak the user interface. Speaking is enabled by default, so if you installed the voice file Rockbox will speak. - + <b>Crea il file vocale</b><br/>Il file vocale consente a Rockbox di pronunciare le voci dell'interfaccia. La sintesi vocale è abilitata per impostazione predefinita: se installi il file vocale, Rockbox parlerà. @@ -2512,27 +2545,27 @@ Network error: %1. Please check your network and proxy settings. mountpoint unknown or invalid - + punto di montaggio sconosciuto o non valido Mountpoint: - + Punto di montaggio: device unknown or invalid - + dispositivo sconosciuto o non valido Device: - + Dispositivo: Backup && &Uninstallation - + Backup e &disinstallazione @@ -2547,17 +2580,17 @@ Network error: %1. Please check your network and proxy settings. <b>Uninstall Rockbox from your audio player.</b><br/>This will leave the bootloader in place (you need to remove it manually). - <b>Disinstallare Rockbox dal player audio.</b><br/>Questo lascerà il bootloader al suo posto (si dovrà rimuoverlo manualmente). + <b>Disinstalla Rockbox dal lettore audio.</b><br/>Il bootloader non verrà rimosso: devi rimuoverlo manualmente. Backup - Backup + Backup <html><head/><body><p><span style=" font-weight:600;">Backup current installation.</span></p><p>Create a backup by archiving the contents of the Rockbox installation folder.</p></body></html> - + <html><head/><body><p><span style=" font-weight:600;">Esegui il backup dell'installazione corrente.</span></p><p>Crea un backup archiviando il contenuto della cartella di installazione di Rockbox.</p></body></html> &Manual @@ -2580,7 +2613,7 @@ Network error: %1. Please check your network and proxy settings. System &Trace - + &Diagnostica di sistema @@ -2590,12 +2623,12 @@ Network error: %1. Please check your network and proxy settings. Welcome to Rockbox Utility, the installation and housekeeping tool for Rockbox. - + Benvenuto in Rockbox Utility, lo strumento di installazione e manutenzione di Rockbox. Rockbox Logo - + Logo di Rockbox @@ -2605,7 +2638,7 @@ Network error: %1. Please check your network and proxy settings. Install Rockbox Utility on player - Installa l'utility Rockbox sul tuo player + Installa Rockbox Utility sul lettore @@ -2641,7 +2674,7 @@ Network error: %1. Please check your network and proxy settings. &Eject - + Rimozione &sicura @@ -2686,12 +2719,12 @@ Network error: %1. Please check your network and proxy settings. &Install Voice File - &Installa file Voice + &Installa file vocale Create &Talk Files - &Crea file Talk + &Crea file vocali .talk @@ -2726,12 +2759,12 @@ Network error: %1. Please check your network and proxy settings. Create &Voice File - Crea &file Voce + Crea &file vocale Create Voice File - Crea file Voce + Crea file vocale @@ -2741,7 +2774,7 @@ Network error: %1. Please check your network and proxy settings. Show &Changelog - + Mostra il &changelog @@ -2749,265 +2782,265 @@ Network error: %1. Please check your network and proxy settings. Selective Installation - + Installazione selettiva Rockbox version to install - + Versione di Rockbox da installare Version information not available yet. - + Le informazioni sulla versione non sono ancora disponibili. Rockbox components to install - + Componenti di Rockbox da installare &Bootloader - + &Bootloader Some plugins require additional data files. - + Alcuni plugin richiedono file di dati aggiuntivi. Install prerendered voice file. - + Installa un file vocale già pronto. Plugin Data - + Dati dei Plugin &Manual - &Manuale + &Manuale &Voice File - + File &vocale The main Rockbox firmware. - + Il firmware principale di Rockbox. Fonts - + Font &Rockbox - + &Rockbox Additional fonts for the User Interface. - + Font aggiuntivi per l'interfaccia utente. The bootloader is required for starting Rockbox. Only necessary for first time install. - + Il bootloader serve per avviare Rockbox. È necessario solo durante la prima installazione. Customize - + Personalizza Themes - + Temi Themes allow adjusting the user interface of Rockbox. Use "Customize" to select themes. - + I temi consentono di modificare l'interfaccia utente di Rockbox. Usa «Personalizza» per selezionarli. Save a copy of the manual on the player. - + Salva una copia del manuale sul lettore. &Install - &Installa + &Installa This is the latest stable release available. - + Questa è l'ultima versione stabile disponibile. This will eventually become the next Rockbox version. Install it to help testing. - + Questa diventerà la prossima versione di Rockbox. Installala per contribuire ai test. Stable Release (Version %1) - + Versione stabile (%1) Development Version (Revison %1) - + Versione di sviluppo (revisione %1) Release Candidate (Revison %1) - + Release candidate (revisione %1) The development version is updated on every code change. - + La versione di sviluppo viene aggiornata a ogni modifica del codice. Daily updated development version. - + Versione di sviluppo aggiornata ogni giorno. Not available for the selected version - + Non disponibile per la versione selezionata Daily Build (%1) - + Daily build (%1) The selected player doesn't need a bootloader. - + Il lettore selezionato non richiede un bootloader. The bootloader is required for starting Rockbox. Installation of the bootloader is only necessary on first time installation. - + Il bootloader è necessario per avviare Rockbox, ma va installato soltanto la prima volta. Mountpoint is wrong - + Il punto di montaggio non è corretto No install method known. - Nessun metodo di installazione conosciuto. + Nessun metodo di installazione disponibile. Bootloader detected - Trovato bootloader + Bootloader rilevato Bootloader already installed. Do you want to reinstall the bootloader? - Bootloader già installato. Vuoi reinstallarlo? + Bootloader già installato. Vuoi reinstallarlo? Bootloader installation skipped - Installazione bootloader saltata + Installazione del bootloader ignorata Create Bootloader backup - Crea un backup del bootloader + Crea il backup del bootloader You can create a backup of the original bootloader file. Press "Yes" to select an output folder on your computer to save the file to. The file will get placed in a new folder "%1" created below the selected folder. Press "No" to skip this step. - Puoi creare un backup del bootloader originale. Premi "Si" e seleziona una cartella sul tuo pc dove salvare il file. Il file verrà salvato in una nuova cartella "%1" all'interno della cartella selezionata. -Premi "No" per saltare questo passo. + Puoi creare una copia di backup del file originale del bootloader. Premi "Sì" e seleziona sul computer la cartella in cui salvarla. Il file verrà inserito in una nuova cartella "%1", creata all'interno della cartella selezionata. +Premi "No" per saltare questo passaggio. Browse backup folder - Scegli la cartella per il backup + Scegli la cartella per il backup Prerequisites - Prerequisiti + Prerequisiti Bootloader installation aborted - Installazione bootloader interrotta + Installazione del bootloader interrotta Bootloader files (%1) - + File del bootloader (%1) All files (*) - + Tutti i file (*) Select firmware file - Selezionare il file del firmware + Seleziona il file del firmware Error opening firmware file - Errore nell'aprire il file del firmware + Errore durante l'apertura del file del firmware Error reading firmware file - + Errore durante la lettura del file del firmware Backup error - Errore durante il backup + Errore durante il backup Could not create backup file. Continue? - Impossibile creare un backup. Continuare comunque? + Impossibile creare un backup. Vuoi continuare comunque? Manual steps required - Richiesto intervento manuale + Sono necessari passaggi manuali Your installation doesn't require any plugin data files, skipping. - + L'installazione non richiede file di dati per i plugin: questo passaggio verrà saltato. Continue with installation? - + Vuoi continuare con l'installazione? Really continue? - Vuoi continuare veramente? + Vuoi davvero continuare? Aborted! @@ -3020,7 +3053,7 @@ Premi "No" per saltare questo passo. Save system trace log - + Salva il registro diagnostico del sistema @@ -3028,32 +3061,32 @@ Premi "No" per saltare questo passo. System Trace - + Diagnostica di sistema System State trace - + Informazioni diagnostiche sullo stato del sistema &Close - + &Chiudi &Save - + &Salva &Refresh - &Aggiorna + &Aggiorna Save &previous - + Salva &precedente @@ -3066,7 +3099,7 @@ Premi "No" per saltare questo passo. <b>Username</b><br/>%1<hr/> - <b>Nome utente<7b><br/>%1<hr/> + <b>Nome utente</b><br/>%1<hr/> @@ -3091,32 +3124,32 @@ Premi "No" per saltare questo passo. Mountpoint - + Punto di montaggio Label - + Etichetta Free - + Spazio libero Total - + Spazio totale Type - + Tipo <tr><td>%1</td><td>%4</td><td>%2 GiB</td><td>%3 GiB</td><td>%5</td></tr> - + <tr><td>%1</td><td>%4</td><td>%2 GiB</td><td>%3 GiB</td><td>%5</td></tr> @@ -3142,27 +3175,27 @@ Premi "No" per saltare questo passo. Guest - Ospite + Utente guest Admin - Amministratore + Utente amministratore User - Utente + Utente Error - Errore + Errore (no description available) - (nessuna descrizione disponibile) + (nessuna descrizione disponibile) @@ -3170,52 +3203,52 @@ Premi "No" per saltare questo passo. Espeak TTS Engine - + Motore TTS eSpeak Espeak-ng TTS Engine - + Motore TTS eSpeak NG Mimic TTS Engine - + Motore TTS Mimic Flite TTS Engine - + Motore TTS Flite Swift TTS Engine - + Motore TTS Swift SAPI4 TTS Engine - + Motore TTS SAPI4 SAPI5 TTS Engine - + Motore TTS SAPI5 MS Speech Platform - + Microsoft Speech Platform Festival TTS Engine - + Motore TTS Festival OS X System Engine - + Motore TTS di sistema di OS X @@ -3223,27 +3256,27 @@ Premi "No" per saltare questo passo. Voice: - Voce: + Voce: Speed (words/min): - + Velocità (parole/min): Pitch (0 for default): - + Tono (0 per impostazione predefinita): Could not voice string - + Impossibile vocalizzare il testo Could not convert intermediate file - + Impossibile convertire il file intermedio @@ -3284,7 +3317,7 @@ Premi "No" per saltare questo passo. Festival could not be started - + Impossibile avviare Festival @@ -3322,12 +3355,12 @@ Premi "No" per saltare questo passo. Could not copy the SAPI script - + Impossibile copiare lo script SAPI Could not start SAPI process - + Impossibile avviare il processo SAPI @@ -3340,37 +3373,37 @@ Premi "No" per saltare questo passo. Talk file creation aborted - Creazione del TalkFile interrotta + Creazione dei file vocali .talk interrotta Copying Talkfiles... - + Copia dei file vocali .talk in corso... File copy aborted - + Copia del file interrotta Cleaning up... - + Pulizia in corso... Finished - + Terminato Finished creating Talk files - Creazione Talkfile conclusa + Creazione dei file vocali .talk completata Starting Talk file generation for folder %1 - + Avvio della generazione dei file vocali .talk per la cartella %1 @@ -3383,54 +3416,54 @@ Premi "No" per saltare questo passo. Starting TTS Engine - + Avvio del motore TTS Init of TTS engine failed - + Inizializzazione del motore TTS non riuscita Starting Encoder Engine - + Avvio del codificatore Init of Encoder engine failed - + Inizializzazione del codificatore non riuscita Voicing entries... - + Vocalizzazione di tutti gli elementi in corso... Encoding files... - Codififca file in corso... + Codifica dei file in corso... Voicing aborted - + Vocalizzazione interrotta Voicing of %1 failed: %2 - Sintesi vocale di %1 fallita: %2 + Vocalizzazione di %1 non riuscita: %2 Encoding aborted - + Codifica interrotta Encoding of %1 failed - Codifica di %1 fallita + Codifica di %1 non riuscita @@ -3468,7 +3501,7 @@ Premi "No" per saltare questo passo. &Cancel - &annulla + &Annulla @@ -3509,8 +3542,8 @@ Please check your network and proxy settings. Download size %L1 kiB (%n item(s)) - Grandezza download %L1 kiB (%n oggetti) - + Dimensione del download: %L1 kiB (%n elemento) + Dimensione del download: %L1 kiB (%n elementi) @@ -3563,7 +3596,7 @@ HTTP response code: %1 Select - + Seleziona @@ -3573,7 +3606,7 @@ HTTP response code: %1 No themes selected, skipping - + Nessun tema selezionato, questo passaggio viene saltato @@ -3645,12 +3678,12 @@ HTTP response code: %1 Uninstalling %1... - + Disinstallazione di %1 in corso... Could not delete %1 - + Impossibile eliminare %1 @@ -3664,18 +3697,18 @@ HTTP response code: %1 <li>Permissions insufficient for bootloader installation. Administrator priviledges are necessary.</li> - <li>Permessi insufficenti per installare il bootloader. + <li>I permessi per installare il bootloader sono insufficienti. Sono necessari i privilegi di amministratore.</li> <li>Target mismatch detected.<br/>Installed target: %1<br/>Selected target: %2.</li> - + <li>Rilevata una mancata corrispondenza del modello.<br/>Modello installato: %1<br/>Modello selezionato: %2.</li> Problem detected: - Problema individuato: + Problema individuato: @@ -3683,7 +3716,7 @@ Sono necessari i privilegi di amministratore.</li> Starting Voicefile generation - Generazione del Voicefile in corso + Creazione del file vocale in corso... @@ -3693,37 +3726,37 @@ Sono necessari i privilegi di amministratore.</li> Extracted voice corrections file from installation - + File delle correzioni di pronuncia estratto dall'installazione Using internal voice corrections file - + Utilizzo del file interno delle correzioni di pronuncia Extracted language enumeration file from installation - + File con l'elenco delle stringhe della lingua estratto dall'installazione Extracted voice strings from installation - + Stringhe vocali estratte dall'installazione Extracted voice strings incompatible - + Le stringhe vocali estratte sono incompatibili Could not retrieve strings from installation, downloading - + Impossibile recuperare le stringhe dall'installazione, le scarico Downloading voice info... - + Scaricamento delle informazioni vocali in corso... @@ -3753,22 +3786,22 @@ Sono necessari i privilegi di amministratore.</li> Reading strings... - + Lettura delle stringhe in corso... Creating voicefiles... - + Creazione dei file vocali in corso... Cleaning up... - + Pulizia in corso... Finished - + Terminato @@ -3829,29 +3862,29 @@ Sono necessari i privilegi di amministratore.</li> Extraction failed! - + Estrazione non riuscita! Not enough disk space! Aborting. - Spazio du disco insufficente! Interruzione in corso. + Spazio su disco insufficiente! Operazione interrotta. Package installation finished successfully. - + L'installazione del pacchetto è stata completata correttamente. Download error: received HTTP error %1 %2 - Errore durante il download: errore HTTP %1. {1 -%2?} + Errore durante il download: errore HTTP %1 +%2 Download finished (cache used). - Download concluso (cache usata). + Download terminato (è stata utilizzata la cache). @@ -3874,17 +3907,17 @@ Sono necessari i privilegi di amministratore.</li> Creating output path failed - + Creazione del percorso di destinazione non riuscita Creating output file failed - + Creazione del file di destinazione non riuscita Error during Zip operation - + Errore durante l'operazione ZIP @@ -3902,7 +3935,7 @@ Sono necessari i privilegi di amministratore.</li> Installer and housekeeping utility for the Rockbox open source digital audio player firmware.<br/>© The Rockbox Team.<br/>Released under the GNU General Public License v2.<br/>Uses icons by the <a href="http://tango.freedesktop.org/">Tango Project</a>.<br/><center><a href="https://www.rockbox.org">https://www.rockbox.org</a></center> - + Utility di installazione e manutenzione per Rockbox, firmware open source per lettori audio digitali.<br/>© The Rockbox Team.<br/>Distribuito secondo i termini della GNU General Public License v2.<br/>Usa le icone del <a href="http://tango.freedesktop.org/">Tango Project</a>.<br/><center><a href="https://www.rockbox.org">https://www.rockbox.org</a></center> @@ -3917,7 +3950,7 @@ Sono necessari i privilegi di amministratore.</li> L&ibraries - + L&ibrerie From 51d7d56803379aeefc3ac3a7f0e1a7d564912b1c Mon Sep 17 00:00:00 2001 From: Jerry Shen Date: Tue, 11 Aug 2026 22:45:40 -0700 Subject: [PATCH 082/408] ap80max: New hosted port HibyOS-based target: keymaps, LED, powermgmt, bootloader and sim support, plus cabbiev2 for the new 360x640x16 screen. Opus 5 helped w/ debugging and the initial scaffolding. Updated name to FB_STRIDE_MISMATCH, added simulator bmp and specs. Change-Id: If797ac6581cf75d9d1fcf36e615ee03cf6f905d6 --- apps/SOURCES | 2 + apps/bitmaps/native/SOURCES | 2 +- apps/keymaps/keymap-hidizsap80max.c | 246 ++++++++++++++++++ apps/plugins/SOURCES.app_build | 2 +- apps/plugins/SUBDIRS.app_build | 2 +- apps/plugins/battery_bench.c | 2 +- apps/plugins/bitmaps/mono/SOURCES | 3 +- apps/plugins/bitmaps/native/SOURCES | 7 +- apps/plugins/blackjack.c | 2 +- apps/plugins/brickmania.c | 2 +- apps/plugins/bubbles.c | 11 + apps/plugins/calculator.c | 2 +- apps/plugins/calendar.c | 2 +- apps/plugins/chessbox/chessbox_pgn.h | 2 +- apps/plugins/chessclock.c | 2 +- apps/plugins/chip8.c | 2 +- apps/plugins/clix.c | 2 +- apps/plugins/cube.c | 2 +- apps/plugins/doom/i_video.c | 2 +- apps/plugins/flipit.c | 2 +- apps/plugins/fractals/fractal.h | 2 +- apps/plugins/imageviewer/imageviewer_button.h | 2 +- apps/plugins/invadrox.c | 20 +- apps/plugins/jewels.c | 2 +- apps/plugins/lib/keymaps.h | 3 +- apps/plugins/lib/pluginlib_actions.c | 2 +- apps/plugins/midi/midiplay.c | 2 +- apps/plugins/minesweeper.c | 2 +- apps/plugins/mp3_encoder.c | 2 +- apps/plugins/mpegplayer/mpeg_settings.c | 2 +- apps/plugins/mpegplayer/mpegplayer.c | 2 +- apps/plugins/oscilloscope.c | 2 +- apps/plugins/pacbox/pacbox.h | 2 +- apps/plugins/pegbox.c | 2 +- apps/plugins/pong.c | 2 +- apps/plugins/reversi/reversi-gui.h | 2 +- apps/plugins/rockblox.c | 19 +- apps/plugins/rockboy/rockboy.c | 2 +- apps/plugins/rockpaint.c | 2 +- apps/plugins/sliding_puzzle.c | 2 +- apps/plugins/snake.c | 2 +- apps/plugins/snake2.c | 2 +- apps/plugins/sokoban.c | 2 +- apps/plugins/solitaire.c | 2 +- apps/plugins/spacerocks.c | 2 +- apps/plugins/star.c | 2 +- apps/plugins/sudoku/sudoku.h | 2 +- apps/plugins/test_touchscreen.c | 2 +- apps/plugins/text_viewer/tv_button.h | 2 +- apps/plugins/vu_meter.c | 2 +- apps/plugins/wormlet.c | 4 +- apps/plugins/xobox.c | 2 +- apps/plugins/zxbox/keymaps.h | 2 +- backdrops/cabbiev2.360x640x16.bmp | Bin 0 -> 691254 bytes bootloader/hibyos_linux.c | 22 +- docs/CREDITS | 1 + firmware/SOURCES | 15 +- firmware/export/config.h | 3 + firmware/export/config/hidizsap80max.h | 105 ++++++++ .../hosted/hiby/ap80max/button-ap80max.c | 83 ++++++ .../hosted/hiby/ap80max/button-target.h | 67 +++++ .../target/hosted/hiby/ap80max/led-ap80max.c | 117 +++++++++ .../hosted/hiby/ap80max/powermgmt-ap80max.c | 39 +++ firmware/target/hosted/hiby/lcd-target.h | 6 + firmware/target/hosted/lcd-linuxfb.c | 29 +++ firmware/target/hosted/sdl/sim-ui-defines.h | 7 + firmware/usb.c | 4 +- tools/builds.pm | 4 + tools/configure | 29 ++- uisimulator/bitmaps/UI-hidizsap80max.bmp | Bin 0 -> 1020870 bytes uisimulator/buttonmap/SOURCES | 2 + uisimulator/buttonmap/hidizs-ap80max.c | 57 ++++ wps/WPSLIST | 5 + wps/cabbiev2.360x640x16.wps | 87 +++++++ wps/cabbiev2/battery-360x640x16.bmp | Bin 0 -> 58054 bytes wps/cabbiev2/lock-360x640x16.bmp | Bin 0 -> 6782 bytes wps/cabbiev2/pb-360x640x16.bmp | Bin 0 -> 6654 bytes wps/cabbiev2/playmode-360x640x16.bmp | Bin 0 -> 15714 bytes wps/cabbiev2/repeat-360x640x16.bmp | Bin 0 -> 9798 bytes wps/cabbiev2/shuffle-360x640x16.bmp | Bin 0 -> 4926 bytes wps/cabbiev2/volume-360x640x16.bmp | Bin 0 -> 44134 bytes wps/cabbiev2/wpsbackdrop-360x640x16.bmp | Bin 0 -> 921670 bytes 82 files changed, 1023 insertions(+), 62 deletions(-) create mode 100644 apps/keymaps/keymap-hidizsap80max.c create mode 100644 backdrops/cabbiev2.360x640x16.bmp create mode 100644 firmware/export/config/hidizsap80max.h create mode 100644 firmware/target/hosted/hiby/ap80max/button-ap80max.c create mode 100644 firmware/target/hosted/hiby/ap80max/button-target.h create mode 100644 firmware/target/hosted/hiby/ap80max/led-ap80max.c create mode 100644 firmware/target/hosted/hiby/ap80max/powermgmt-ap80max.c create mode 100644 uisimulator/bitmaps/UI-hidizsap80max.bmp create mode 100644 uisimulator/buttonmap/hidizs-ap80max.c create mode 100644 wps/cabbiev2.360x640x16.wps create mode 100644 wps/cabbiev2/battery-360x640x16.bmp create mode 100644 wps/cabbiev2/lock-360x640x16.bmp create mode 100644 wps/cabbiev2/pb-360x640x16.bmp create mode 100644 wps/cabbiev2/playmode-360x640x16.bmp create mode 100644 wps/cabbiev2/repeat-360x640x16.bmp create mode 100644 wps/cabbiev2/shuffle-360x640x16.bmp create mode 100644 wps/cabbiev2/volume-360x640x16.bmp create mode 100644 wps/cabbiev2/wpsbackdrop-360x640x16.bmp diff --git a/apps/SOURCES b/apps/SOURCES index 923e979af5..bf39273405 100644 --- a/apps/SOURCES +++ b/apps/SOURCES @@ -304,4 +304,6 @@ keymaps/keymap-rgnano.c keymaps/keymap-ctru.c #elif CONFIG_KEYPAD == HIBY_R3PROII_PAD keymaps/keymap-hibyr3proii.c +#elif CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD +keymaps/keymap-hidizsap80max.c #endif diff --git a/apps/bitmaps/native/SOURCES b/apps/bitmaps/native/SOURCES index f0149175f3..9812851388 100644 --- a/apps/bitmaps/native/SOURCES +++ b/apps/bitmaps/native/SOURCES @@ -46,7 +46,7 @@ toolsicon.130x130x16.bmp hibyicon.70x70x16.bmp rockboxicon.70x70x16.bmp toolsicon.70x70x16.bmp -#elif (defined(XDUOO_X3II) || defined(XDUOO_X20) || defined(EROS_Q) || defined(SURFANS_F28) || defined(HIBY_R3PROII) || defined(HIBY_R1)) +#elif (defined(XDUOO_X3II) || defined(XDUOO_X20) || defined(EROS_Q) || defined(SURFANS_F28) || defined(HIBY_R3PROII) || defined(HIBY_R1) || defined(HIDIZS_AP80MAX)) hibyicon.130x130x16.bmp rockboxicon.130x130x16.bmp toolsicon.130x130x16.bmp diff --git a/apps/keymaps/keymap-hidizsap80max.c b/apps/keymaps/keymap-hidizsap80max.c new file mode 100644 index 0000000000..ca23b3b050 --- /dev/null +++ b/apps/keymaps/keymap-hidizsap80max.c @@ -0,0 +1,246 @@ +/*************************************************************************** + * __________ __ ___ + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025 Solomon Peachy + * Copyright (C) 2026 by Jerry Shen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include +#include + +#include "config.h" +#include "action.h" +#include "button.h" +#include "settings.h" + +static const struct button_mapping button_context_standard[] = +{ + { ACTION_STD_KEYLOCK, BUTTON_POWER|BUTTON_PLAY, BUTTON_POWER }, + + { ACTION_STD_PREV, BUTTON_SCROLL_BACK, BUTTON_NONE }, + { ACTION_STD_NEXT, BUTTON_SCROLL_FWD, BUTTON_NONE }, + + { ACTION_STD_OK, BUTTON_POWER|BUTTON_REL, BUTTON_POWER }, + { ACTION_STD_OK, BUTTON_NEXT|BUTTON_REL, BUTTON_NEXT }, + { ACTION_STD_CANCEL, BUTTON_PREV|BUTTON_REL, BUTTON_PREV }, + + { ACTION_STD_OK, BUTTON_PLAY|BUTTON_REL, BUTTON_PLAY }, + { ACTION_STD_CONTEXT, BUTTON_PLAY|BUTTON_REPEAT, BUTTON_PLAY }, + + LAST_ITEM_IN_LIST +}; + + +static const struct button_mapping button_context_wps[] = +{ + { ACTION_WPS_MENU, BUTTON_POWER|BUTTON_REL, BUTTON_POWER }, + { ACTION_WPS_PLAY, BUTTON_PLAY|BUTTON_REL, BUTTON_PLAY }, + { ACTION_WPS_STOP, BUTTON_PLAY|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_WPS_VOLDOWN, BUTTON_SCROLL_FWD, BUTTON_NONE }, + { ACTION_WPS_VOLUP, BUTTON_SCROLL_BACK, BUTTON_NONE }, + { ACTION_WPS_SKIPNEXT, BUTTON_NEXT|BUTTON_REL, BUTTON_NEXT }, + { ACTION_WPS_SKIPPREV, BUTTON_PREV|BUTTON_REL, BUTTON_PREV }, + { ACTION_WPS_SEEKFWD, BUTTON_NEXT|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_WPS_STOPSEEK, BUTTON_NEXT|BUTTON_REL, BUTTON_NEXT|BUTTON_REPEAT }, + { ACTION_WPS_SEEKBACK, BUTTON_PREV|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_WPS_STOPSEEK, BUTTON_PREV|BUTTON_REL, BUTTON_PREV|BUTTON_REPEAT }, + { ACTION_STD_KEYLOCK, BUTTON_POWER|BUTTON_PLAY, BUTTON_POWER }, + + LAST_ITEM_IN_LIST +}; + + +static const struct button_mapping button_context_list[] = +{ + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD), +}; + + +static const struct button_mapping button_context_tree[] = +{ + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_LIST), +}; + + +static const struct button_mapping button_context_listtree_scroll_with_combo[] = +{ + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_CUSTOM | CONTEXT_TREE), +}; + + +static const struct button_mapping button_context_listtree_scroll_without_combo[] = +{ + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_CUSTOM | CONTEXT_TREE), +}; + + +static const struct button_mapping button_context_settings[] = +{ + { ACTION_SETTINGS_DEC, BUTTON_SCROLL_FWD, BUTTON_NONE }, + { ACTION_SETTINGS_INC, BUTTON_SCROLL_BACK, BUTTON_NONE }, + { ACTION_SETTINGS_INC, BUTTON_NEXT, BUTTON_NONE }, + { ACTION_SETTINGS_DEC, BUTTON_PREV, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD), +}; + + +static const struct button_mapping button_context_settings_right_is_inc[] = +{ + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD), +}; + + +static const struct button_mapping button_context_mainmenu[] = +{ + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_TREE), +}; + + +static const struct button_mapping button_context_yesno[] = +{ + { ACTION_YESNO_ACCEPT, BUTTON_PLAY|BUTTON_REL, BUTTON_PLAY }, + { ACTION_YESNO_ACCEPT, BUTTON_POWER|BUTTON_REL, BUTTON_POWER }, + { ACTION_STD_CANCEL, BUTTON_PREV|BUTTON_REL, BUTTON_PREV }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD), +}; + + +static const struct button_mapping button_context_colorchooser[] = +{ + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_SETTINGS), +}; + + +static const struct button_mapping button_context_eq[] = +{ + { ACTION_SETTINGS_INCBIGSTEP, BUTTON_PLAY|BUTTON_SCROLL_BACK, BUTTON_NONE }, + { ACTION_SETTINGS_DECBIGSTEP, BUTTON_PLAY|BUTTON_SCROLL_FWD, BUTTON_NONE }, + + { ACTION_SETTINGS_INC, BUTTON_SCROLL_BACK, BUTTON_NONE }, + { ACTION_SETTINGS_DEC, BUTTON_SCROLL_FWD, BUTTON_NONE }, + + { ACTION_NONE, BUTTON_PREV, BUTTON_NONE }, + { ACTION_NONE, BUTTON_NEXT, BUTTON_NONE }, + { ACTION_STD_PREV, BUTTON_PREV|BUTTON_REL, BUTTON_PREV }, + { ACTION_STD_PREVREPEAT, BUTTON_PREV|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_STD_NEXT, BUTTON_NEXT|BUTTON_REL, BUTTON_NEXT }, + { ACTION_STD_NEXTREPEAT, BUTTON_NEXT|BUTTON_REPEAT, BUTTON_NONE }, + + { ACTION_STD_OK, BUTTON_PLAY|BUTTON_REL, BUTTON_PLAY }, + { ACTION_STD_CANCEL, BUTTON_POWER|BUTTON_REL, BUTTON_POWER }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_SETTINGS), +}; + + +static const struct button_mapping button_context_keyboard[] = +{ + { ACTION_KBD_LEFT, BUTTON_SCROLL_BACK, BUTTON_NONE }, + { ACTION_KBD_RIGHT, BUTTON_SCROLL_FWD, BUTTON_NONE }, + { ACTION_KBD_CURSOR_LEFT, BUTTON_PLAY | BUTTON_SCROLL_BACK, BUTTON_NONE }, + { ACTION_KBD_CURSOR_RIGHT, BUTTON_PLAY | BUTTON_SCROLL_FWD, BUTTON_NONE }, + { ACTION_KBD_DONE, BUTTON_PLAY | BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_KBD_SELECT, BUTTON_PLAY | BUTTON_REL, BUTTON_NONE }, + { ACTION_KBD_ABORT, BUTTON_POWER, BUTTON_NONE }, + { ACTION_KBD_UP, BUTTON_NEXT, BUTTON_NONE }, + { ACTION_KBD_UP, BUTTON_NEXT | BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_KBD_DOWN, BUTTON_PREV, BUTTON_NONE }, + { ACTION_KBD_DOWN, BUTTON_PREV | BUTTON_REPEAT, BUTTON_NONE }, + + LAST_ITEM_IN_LIST +}; + + +static const struct button_mapping button_context_bmark[] = +{ + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_LIST), +}; + + +static const struct button_mapping button_context_time[] = +{ + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_SETTINGS), +}; + + +static const struct button_mapping button_context_quickscreen[] = +{ + { ACTION_QS_TOP, BUTTON_SCROLL_BACK, BUTTON_NONE }, + { ACTION_QS_DOWN, BUTTON_SCROLL_FWD, BUTTON_NONE }, + { ACTION_QS_LEFT, BUTTON_PREV|BUTTON_REL, BUTTON_NONE }, + { ACTION_QS_LEFT, BUTTON_PREV|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_QS_RIGHT, BUTTON_NEXT|BUTTON_REL, BUTTON_NONE }, + { ACTION_QS_RIGHT, BUTTON_NEXT|BUTTON_REPEAT, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD) +}; + + +static const struct button_mapping button_context_pitchscreen[] = +{ + { ACTION_PS_DEC_SMALL, BUTTON_SCROLL_FWD, BUTTON_NONE }, + { ACTION_PS_INC_SMALL, BUTTON_SCROLL_BACK, BUTTON_NONE }, + { ACTION_PS_NUDGE_RIGHT, BUTTON_NEXT, BUTTON_NONE }, + { ACTION_PS_NUDGE_LEFT, BUTTON_PREV, BUTTON_NONE }, + { ACTION_PS_EXIT, BUTTON_POWER, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD) +}; + + +static const struct button_mapping button_context_radio[] = +{ + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_SETTINGS) +}; + + +const struct button_mapping* target_get_context_mapping(int context) +{ + switch (context & ~CONTEXT_LOCKED) + { + case CONTEXT_STD: { return button_context_standard; } + case CONTEXT_WPS: { return button_context_wps; } + case CONTEXT_LIST: { return button_context_list; } + case CONTEXT_MAINMENU: { return button_context_mainmenu; } + case CONTEXT_CUSTOM | CONTEXT_TREE: { return button_context_tree; } + case CONTEXT_SETTINGS: { return button_context_settings; } + case CONTEXT_SETTINGS_COLOURCHOOSER: { return button_context_colorchooser; } + case CONTEXT_SETTINGS_EQ: { return button_context_eq; } + case CONTEXT_SETTINGS_TIME: { return button_context_time; } + case CONTEXT_YESNOSCREEN: { return button_context_yesno; } + case CONTEXT_KEYBOARD: { return button_context_keyboard; } + case CONTEXT_FM: { return button_context_radio; } + case CONTEXT_BOOKMARKSCREEN: { return button_context_bmark; } + case CONTEXT_QUICKSCREEN: { return button_context_quickscreen; } + case CONTEXT_PITCHSCREEN: { return button_context_pitchscreen; } + case CONTEXT_CUSTOM | CONTEXT_SETTINGS: + case CONTEXT_SETTINGS_RECTRIGGER: { return button_context_settings_right_is_inc; } + case CONTEXT_TREE: + { + if(global_settings.hold_lr_for_scroll_in_list) + { + return button_context_listtree_scroll_without_combo; + } + return button_context_listtree_scroll_with_combo; + } + } + + return button_context_standard; +} diff --git a/apps/plugins/SOURCES.app_build b/apps/plugins/SOURCES.app_build index cdd764dc27..439a6dd29c 100644 --- a/apps/plugins/SOURCES.app_build +++ b/apps/plugins/SOURCES.app_build @@ -1,4 +1,4 @@ -#if !defined(HAVE_TOUCHSCREEN) || defined(DX50) || defined(DX90) || defined(HIBY_R3PROII) || defined(HIBY_R1) +#if !defined(HAVE_TOUCHSCREEN) || defined(DX50) || defined(DX90) || defined(HIBY_R3PROII) || defined(HIBY_R1) || defined(HIDIZS_AP80MAX) /* In devices running RockBox as an application, but having a keypad */ #include "SOURCES" #else diff --git a/apps/plugins/SUBDIRS.app_build b/apps/plugins/SUBDIRS.app_build index 5618f41afe..d4c382050c 100644 --- a/apps/plugins/SUBDIRS.app_build +++ b/apps/plugins/SUBDIRS.app_build @@ -1,4 +1,4 @@ -#if !defined(HAVE_TOUCHSCREEN) || defined(DX50) || defined(DX90) || defined(HIBY_R3PROII) || defined(HIBY_R1) +#if !defined(HAVE_TOUCHSCREEN) || defined(DX50) || defined(DX90) || defined(HIBY_R3PROII) || defined(HIBY_R1) || defined(HIDIZS_AP80MAX) /* This is for devices having a keypad, running RockBox as an application */ #include "SUBDIRS" diff --git a/apps/plugins/battery_bench.c b/apps/plugins/battery_bench.c index ec7ad45bb8..4b2d90588c 100644 --- a/apps/plugins/battery_bench.c +++ b/apps/plugins/battery_bench.c @@ -243,7 +243,7 @@ struct battery_tables_t { #define BATTERY_ON_TXT "Play" #define BATTERY_OFF_TXT "Power" -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/bitmaps/mono/SOURCES b/apps/plugins/bitmaps/mono/SOURCES index 067942a70e..d864ef5ea7 100644 --- a/apps/plugins/bitmaps/mono/SOURCES +++ b/apps/plugins/bitmaps/mono/SOURCES @@ -21,7 +21,8 @@ bubbles_bubble.220x176x1.bmp #elif ((LCD_WIDTH == 240) && (LCD_HEIGHT == 240)) || \ ((LCD_WIDTH == 320) && (LCD_HEIGHT == 240)) || \ ((LCD_WIDTH == 240) && (LCD_HEIGHT >= 320)) || \ - ((LCD_WIDTH == 360) && (LCD_HEIGHT == 400)) + ((LCD_WIDTH == 360) && (LCD_HEIGHT == 400)) || \ + ((LCD_WIDTH == 360) && (LCD_HEIGHT == 640)) bubbles_bubble.320x240x1.bmp #elif ((LCD_WIDTH == 640) && (LCD_HEIGHT == 480)) || \ ((LCD_WIDTH == 480) && (LCD_HEIGHT >= 640)) diff --git a/apps/plugins/bitmaps/native/SOURCES b/apps/plugins/bitmaps/native/SOURCES index b2286bcc33..e9c852e1b2 100644 --- a/apps/plugins/bitmaps/native/SOURCES +++ b/apps/plugins/bitmaps/native/SOURCES @@ -438,7 +438,7 @@ invadrox_ufo_explode.21x8x16.bmp invadrox_numbers.50x7x16.bmp #if LCD_WIDTH == 480 invadrox_background.480x640x16.bmp -#elif LCD_WIDTH == 360 && LCD_HEIGHT == 400 +#elif LCD_WIDTH == 360 && (LCD_HEIGHT == 400 || LCD_HEIGHT == 640) invadrox_background.360x400x16.bmp #elif LCD_WIDTH == 320 invadrox_background.320x240x16.bmp @@ -513,7 +513,7 @@ jewels.240x240x16.bmp jewels.320x240x16.bmp #elif (LCD_WIDTH == 320) && (LCD_HEIGHT == 240) jewels.320x240x16.bmp -#elif (LCD_WIDTH == 360) && (LCD_HEIGHT == 400) +#elif (LCD_WIDTH == 360) && ((LCD_HEIGHT == 400) || (LCD_HEIGHT == 640)) jewels.360x400x16.bmp #elif ((LCD_WIDTH >= 480) && (LCD_HEIGHT >= 640)) || \ ((LCD_WIDTH >= 640) && (LCD_HEIGHT >= 480)) @@ -947,7 +947,8 @@ superdom_boarditems.176x132x16.bmp superdom_boarditems.320x240x16.bmp #elif ((LCD_WIDTH == 240) && (LCD_HEIGHT == 320)) || \ ((LCD_WIDTH == 240) && (LCD_HEIGHT == 400)) || \ - ((LCD_WIDTH == 360) && (LCD_HEIGHT == 400)) + ((LCD_WIDTH == 360) && (LCD_HEIGHT == 400)) || \ + ((LCD_WIDTH == 360) && (LCD_HEIGHT == 640)) superdom_boarditems.240x320x16.bmp #elif (LCD_WIDTH >= 480 && LCD_HEIGHT >= 640) superdom_boarditems.480x640x16.bmp diff --git a/apps/plugins/blackjack.c b/apps/plugins/blackjack.c index cca73029b2..233c3e760d 100644 --- a/apps/plugins/blackjack.c +++ b/apps/plugins/blackjack.c @@ -591,7 +591,7 @@ enum { #define BJACK_LEFT BUTTON_LEFT -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define BJACK_QUIT BUTTON_POWER #define BJACK_QUIT_NAME "QUIT" diff --git a/apps/plugins/brickmania.c b/apps/plugins/brickmania.c index 4ec3a40f3a..b588000cdb 100644 --- a/apps/plugins/brickmania.c +++ b/apps/plugins/brickmania.c @@ -353,7 +353,7 @@ CONFIG_KEYPAD == SANSA_M200_PAD #define UP BUTTON_UP #define DOWN BUTTON_DOWN -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define QUIT BUTTON_POWER #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/bubbles.c b/apps/plugins/bubbles.c index a647d98fad..374b4cad38 100644 --- a/apps/plugins/bubbles.c +++ b/apps/plugins/bubbles.c @@ -138,6 +138,17 @@ enum { #define XOFS 128 #define MAX_FPS 40 +#elif (LCD_WIDTH == 360) && (LCD_HEIGHT == 640) +#define XOFS 86 +#define YOFS 220 +#define SCORE_TXT_X 28 +#define SCORE_TXT_WIDTH 32 +#define SCORE_TXT_Y 48 +#define LEVEL_TXT_X 27 +#define LEVEL_TXT_WIDTH 32 +#define LEVEL_TXT_Y 5 +#define MAX_FPS 40 + #elif (LCD_WIDTH == 360) && (LCD_HEIGHT == 400) #define XOFS 86 #define YOFS 100 diff --git a/apps/plugins/calculator.c b/apps/plugins/calculator.c index 0b06609573..252618865c 100644 --- a/apps/plugins/calculator.c +++ b/apps/plugins/calculator.c @@ -522,7 +522,7 @@ F3: equal to "=" #define CALCULATOR_CALC BUTTON_MENU #define CALCULATOR_CLEAR BUTTON_BACK -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define CALCULATOR_QUIT BUTTON_POWER #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/calendar.c b/apps/plugins/calendar.c index b1c2662038..2b6813c3f4 100644 --- a/apps/plugins/calendar.c +++ b/apps/plugins/calendar.c @@ -408,7 +408,7 @@ #define CALENDAR_NEXT_MONTH BUTTON_VOL_UP #define CALENDAR_PREV_MONTH BUTTON_VOL_DOWN -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/chessbox/chessbox_pgn.h b/apps/plugins/chessbox/chessbox_pgn.h index eecb70d1c0..d046b1ed8b 100644 --- a/apps/plugins/chessbox/chessbox_pgn.h +++ b/apps/plugins/chessbox/chessbox_pgn.h @@ -566,7 +566,7 @@ #define CB_SCROLL_LEFT (BUTTON_LEFT|BUTTON_REPEAT) #define CB_SCROLL_RIGHT (BUTTON_RIGHT|BUTTON_REPEAT) -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/chessclock.c b/apps/plugins/chessclock.c index 5d4ca183ae..fd9bc92708 100644 --- a/apps/plugins/chessclock.c +++ b/apps/plugins/chessclock.c @@ -384,7 +384,7 @@ #define CHC_SETTINGS_OK BUTTON_SELECT #define CHC_SETTINGS_CANCEL BUTTON_POWER -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define CHC_QUIT BUTTON_POWER #elif (CONFIG_KEYPAD == MA_PAD) diff --git a/apps/plugins/chip8.c b/apps/plugins/chip8.c index 0df928eb77..df583bb04c 100644 --- a/apps/plugins/chip8.c +++ b/apps/plugins/chip8.c @@ -1326,7 +1326,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define CHIP8_KEY9 BUTTON_START #define CHIP8_KEYA BUTTON_X -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #else diff --git a/apps/plugins/clix.c b/apps/plugins/clix.c index 721f2b85fa..356021f826 100644 --- a/apps/plugins/clix.c +++ b/apps/plugins/clix.c @@ -305,7 +305,7 @@ #define CLIX_BUTTON_RIGHT BUTTON_RIGHT #define CLIX_BUTTON_CLICK BUTTON_SELECT -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define CLIX_BUTTON_QUIT BUTTON_POWER #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/cube.c b/apps/plugins/cube.c index fec40eb274..9a48b37a37 100644 --- a/apps/plugins/cube.c +++ b/apps/plugins/cube.c @@ -391,7 +391,7 @@ #define CUBE_PAUSE BUTTON_PLAY #define CUBE_HIGHSPEED BUTTON_BACK -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/doom/i_video.c b/apps/plugins/doom/i_video.c index 1e64c2f40d..5f3eb68cbe 100644 --- a/apps/plugins/doom/i_video.c +++ b/apps/plugins/doom/i_video.c @@ -616,7 +616,7 @@ void I_ShutdownGraphics(void) #define DOOMBUTTON_WEAPON BUTTON_BACK #define DOOMBUTTON_MAP (BUTTON_BACK|BUTTON_REPEAT) -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define DOOMBUTTON_ESC BUTTON_POWER #define DOOMBUTTON_MAP BUTTON_PREV diff --git a/apps/plugins/flipit.c b/apps/plugins/flipit.c index 66bf87cc94..673dadbb3e 100644 --- a/apps/plugins/flipit.c +++ b/apps/plugins/flipit.c @@ -481,7 +481,7 @@ #define FLIPIT_STEP_BY_STEP BUTTON_VOL_UP #define FLIPIT_TOGGLE BUTTON_SELECT -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/fractals/fractal.h b/apps/plugins/fractals/fractal.h index 96a41736e7..84a932d713 100644 --- a/apps/plugins/fractals/fractal.h +++ b/apps/plugins/fractals/fractal.h @@ -491,7 +491,7 @@ #define FRACTAL_PRECISION_DEC BUTTON_BACK #define FRACTAL_RESET BUTTON_PLAY -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define FRACTAL_QUIT BUTTON_POWER #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/imageviewer/imageviewer_button.h b/apps/plugins/imageviewer/imageviewer_button.h index 2396c647b3..cbfce5bc9a 100644 --- a/apps/plugins/imageviewer/imageviewer_button.h +++ b/apps/plugins/imageviewer/imageviewer_button.h @@ -544,7 +544,7 @@ #define IMGVIEW_MENU (BUTTON_PLAY|BUTTON_REPEAT) #define IMGVIEW_SLIDE_SHOW BUTTON_PLAY -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == RG_NANO_PAD diff --git a/apps/plugins/invadrox.c b/apps/plugins/invadrox.c index 8500e2b558..294b4b3c3f 100644 --- a/apps/plugins/invadrox.c +++ b/apps/plugins/invadrox.c @@ -280,7 +280,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define RIGHT BUTTON_RIGHT #define FIRE BUTTON_SELECT -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD @@ -687,6 +687,24 @@ CONFIG_KEYPAD == MROBE500_PAD #define LIVES_X 8 #define MAX_Y 15 +#elif (LCD_WIDTH == 360) && (LCD_HEIGHT == 640) + +/* Hidizs AP80 Pro Max + */ +#define ARCADISH_GRAPHICS +#define PLAYFIELD_X 32 +#define SHIP_Y (PLAYFIELD_Y - 3 * SHIP_HEIGHT) +#define ALIEN_START_Y (UFO_Y + 3 * ALIEN_HEIGHT) +/* Redefine SCORE_Y */ +#undef SCORE_Y +#define SCORE_Y 80 +#define SCORENUM_X (PLAYFIELD_X + NUMBERS_WIDTH) +#define SCORENUM_Y SCORE_Y + (2 * (FONT_HEIGHT + 1) + 1) +#define HISCORENUM_X (LCD_WIDTH - PLAYFIELD_X - 1 - 6 * NUMBERS_WIDTH - 5 * NUM_SPACING) +#define SHIELD_Y (PLAYFIELD_Y - 6 * SHIP_HEIGHT) +#define LIVES_X 10 +#define MAX_Y 30 + #elif (LCD_WIDTH == 360) && (LCD_HEIGHT == 400) /* Shanling Q1 diff --git a/apps/plugins/jewels.c b/apps/plugins/jewels.c index 779febed22..ce2de05330 100644 --- a/apps/plugins/jewels.c +++ b/apps/plugins/jewels.c @@ -366,7 +366,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define HK_SELECT "SELECT" #define HK_CANCEL "BACK" -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/lib/keymaps.h b/apps/plugins/lib/keymaps.h index 71d5b6ad5f..51e9f65395 100644 --- a/apps/plugins/lib/keymaps.h +++ b/apps/plugins/lib/keymaps.h @@ -249,7 +249,7 @@ #define BTN_FIRE BUTTON_SELECT #define BTN_PAUSE BUTTON_POWER -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define BTN_FIRE BUTTON_CENTER #define BTN_PAUSE BUTTON_POWER #define BTN_HAVE_DIAGONAL @@ -318,6 +318,7 @@ (CONFIG_KEYPAD != CREATIVE_ZENXFI2_PAD) && \ (CONFIG_KEYPAD != SHANLING_Q1_PAD) && \ (CONFIG_KEYPAD != HIBY_R3PROII_PAD) && \ + (CONFIG_KEYPAD != HIDIZS_AP80MAX_PAD) && \ (CONFIG_KEYPAD != CTRU_PAD) #define BTN_FIRE BUTTON_BOTTOMLEFT #define BTN_PAUSE BUTTON_TOPLEFT diff --git a/apps/plugins/lib/pluginlib_actions.c b/apps/plugins/lib/pluginlib_actions.c index 5b175a66a4..a45a0a0399 100644 --- a/apps/plugins/lib/pluginlib_actions.c +++ b/apps/plugins/lib/pluginlib_actions.c @@ -529,7 +529,7 @@ const struct button_mapping pla_main_ctx[] = {PLA_SELECT, BUTTON_SELECT, BUTTON_NONE}, {PLA_SELECT_REL, BUTTON_SELECT|BUTTON_REL, BUTTON_SELECT}, {PLA_SELECT_REPEAT, BUTTON_SELECT|BUTTON_REPEAT, BUTTON_NONE}, -#elif (CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD) +#elif (CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD) {PLA_EXIT, BUTTON_POWER, BUTTON_NONE}, #elif (CONFIG_KEYPAD == MA_PAD) {PLA_CANCEL, BUTTON_BACK|BUTTON_REL, BUTTON_BACK}, diff --git a/apps/plugins/midi/midiplay.c b/apps/plugins/midi/midiplay.c index 5ec6e7ede4..647273bf4a 100644 --- a/apps/plugins/midi/midiplay.c +++ b/apps/plugins/midi/midiplay.c @@ -316,7 +316,7 @@ #define MIDI_VOL_DOWN BUTTON_VOL_DOWN #define MIDI_PLAYPAUSE BUTTON_PLAY -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/minesweeper.c b/apps/plugins/minesweeper.c index 41145c90b8..704c659a40 100644 --- a/apps/plugins/minesweeper.c +++ b/apps/plugins/minesweeper.c @@ -441,7 +441,7 @@ CONFIG_KEYPAD == MROBE500_PAD # define MINESWP_DISCOVER (BUTTON_MENU|BUTTON_REPEAT) # define MINESWP_INFO BUTTON_MENU -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == RG_NANO_PAD diff --git a/apps/plugins/mp3_encoder.c b/apps/plugins/mp3_encoder.c index af998487e9..f6423a4329 100644 --- a/apps/plugins/mp3_encoder.c +++ b/apps/plugins/mp3_encoder.c @@ -2578,7 +2578,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define MP3ENC_DONE BUTTON_POWER #define MP3ENC_SELECT BUTTON_SELECT -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/mpegplayer/mpeg_settings.c b/apps/plugins/mpegplayer/mpeg_settings.c index 3c6ec54a0e..8475f26b7f 100644 --- a/apps/plugins/mpegplayer/mpeg_settings.c +++ b/apps/plugins/mpegplayer/mpeg_settings.c @@ -352,7 +352,7 @@ struct mpeg_settings settings; #define MPEG_START_TIME_DOWN BUTTON_DOWN #define MPEG_START_TIME_EXIT BUTTON_BACK -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define MPEG_START_TIME_EXIT BUTTON_POWER #elif CONFIG_KEYPAD == RG_NANO_PAD diff --git a/apps/plugins/mpegplayer/mpegplayer.c b/apps/plugins/mpegplayer/mpegplayer.c index 60522cd56c..761c95cde0 100644 --- a/apps/plugins/mpegplayer/mpegplayer.c +++ b/apps/plugins/mpegplayer/mpegplayer.c @@ -485,7 +485,7 @@ CONFIG_KEYPAD == SANSA_M200_PAD #define MPEG_RW BUTTON_LEFT #define MPEG_FF BUTTON_RIGHT -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == RG_NANO_PAD diff --git a/apps/plugins/oscilloscope.c b/apps/plugins/oscilloscope.c index 4754416ab7..c0bef75cd4 100644 --- a/apps/plugins/oscilloscope.c +++ b/apps/plugins/oscilloscope.c @@ -549,7 +549,7 @@ #define OSCILLOSCOPE_VOL_UP BUTTON_UP #define OSCILLOSCOPE_VOL_DOWN BUTTON_DOWN -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == RG_NANO_PAD diff --git a/apps/plugins/pacbox/pacbox.h b/apps/plugins/pacbox/pacbox.h index 4292e47f18..855e3c536c 100644 --- a/apps/plugins/pacbox/pacbox.h +++ b/apps/plugins/pacbox/pacbox.h @@ -386,7 +386,7 @@ #define PACMAN_1UP BUTTON_VOL_UP #define PACMAN_COIN BUTTON_PLAY -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define PACMAN_UP BUTTON_TOPMIDDLE #define PACMAN_DOWN BUTTON_BOTTOMMIDDLE #define PACMAN_LEFT BUTTON_MIDLEFT diff --git a/apps/plugins/pegbox.c b/apps/plugins/pegbox.c index 7fbac41335..bd17965ef6 100644 --- a/apps/plugins/pegbox.c +++ b/apps/plugins/pegbox.c @@ -710,7 +710,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define LVL_UP_TEXT "UP+MENU" #define LVL_DOWN_TEXT "DOWN+MENU" -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == RG_NANO_PAD diff --git a/apps/plugins/pong.c b/apps/plugins/pong.c index a7aa93724f..5cc79de1d1 100644 --- a/apps/plugins/pong.c +++ b/apps/plugins/pong.c @@ -317,7 +317,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define PONG_RIGHT_UP BUTTON_BACK #define PONG_RIGHT_DOWN BUTTON_RIGHT -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/reversi/reversi-gui.h b/apps/plugins/reversi/reversi-gui.h index c73acb2797..79755f620a 100644 --- a/apps/plugins/reversi/reversi-gui.h +++ b/apps/plugins/reversi/reversi-gui.h @@ -351,7 +351,7 @@ #define REVERSI_BUTTON_MAKE_MOVE BUTTON_SELECT #define REVERSI_BUTTON_MENU BUTTON_MENU -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif (CONFIG_KEYPAD == MA_PAD) diff --git a/apps/plugins/rockblox.c b/apps/plugins/rockblox.c index bdc3795301..b310cf65bd 100644 --- a/apps/plugins/rockblox.c +++ b/apps/plugins/rockblox.c @@ -457,7 +457,7 @@ #define ROCKBLOX_RESTART BUTTON_BACK #define ROCKBLOX_SCROLL_ENABLED 1 -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD @@ -576,6 +576,23 @@ #define LEVEL_Y 142 #define LINES_Y 218 +#elif (LCD_WIDTH == 360) && (LCD_HEIGHT == 640) + +/* 10x20 board of 24px blocks = 240x480, leaving a 100px label column */ +#define BLOCK_WIDTH 24 +#define BLOCK_HEIGHT 24 +#define BOARD_X 8 +#define BOARD_Y 40 +#define LABEL_X 258 +#define SCORE_Y 60 +#define LEVEL_Y 140 +#define LINES_Y 220 +#define HIGH_LABEL_X 258 +#define HIGH_SCORE_Y 320 +#define HIGH_LEVEL_Y 400 +#define PREVIEW_X 258 +#define PREVIEW_Y 480 + #elif (LCD_WIDTH == 360) && (LCD_HEIGHT == 400) #define BLOCK_WIDTH 19 diff --git a/apps/plugins/rockboy/rockboy.c b/apps/plugins/rockboy/rockboy.c index 96fb2c68ba..7d7c9f1b01 100644 --- a/apps/plugins/rockboy/rockboy.c +++ b/apps/plugins/rockboy/rockboy.c @@ -457,7 +457,7 @@ static void setoptions (void) options.SELECT = BUTTON_VOL_UP; options.MENU = BUTTON_POWER; -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/rockpaint.c b/apps/plugins/rockpaint.c index bfde6c53f0..7ed5b4104a 100644 --- a/apps/plugins/rockpaint.c +++ b/apps/plugins/rockpaint.c @@ -392,7 +392,7 @@ #define ROCKPAINT_LEFT BUTTON_LEFT #define ROCKPAINT_RIGHT BUTTON_RIGHT -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == EROSQ_PAD diff --git a/apps/plugins/sliding_puzzle.c b/apps/plugins/sliding_puzzle.c index d3189945ee..562384ac6a 100644 --- a/apps/plugins/sliding_puzzle.c +++ b/apps/plugins/sliding_puzzle.c @@ -347,7 +347,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define PUZZLE_SHUFFLE BUTTON_BACK #define PUZZLE_PICTURE BUTTON_PLAY -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/snake.c b/apps/plugins/snake.c index 49c4ca81e8..2ae29ee6c0 100644 --- a/apps/plugins/snake.c +++ b/apps/plugins/snake.c @@ -304,7 +304,7 @@ dir is the current direction of the snake - 0=up, 1=right, 2=down, 3=left; #define SNAKE_DOWN BUTTON_DOWN #define SNAKE_PLAYPAUSE BUTTON_PLAY -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/snake2.c b/apps/plugins/snake2.c index b5f15b80c2..6b225713e4 100644 --- a/apps/plugins/snake2.c +++ b/apps/plugins/snake2.c @@ -448,7 +448,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define SNAKE2_PLAYPAUSE BUTTON_PLAY #define SNAKE2_PLAYPAUSE_TEXT "PLAY" -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/sokoban.c b/apps/plugins/sokoban.c index 027ad99c7e..625a2679fd 100644 --- a/apps/plugins/sokoban.c +++ b/apps/plugins/sokoban.c @@ -680,7 +680,7 @@ #define BUTTON_SAVE BUTTON_BACK #define BUTTON_SAVE_NAME "BACK" -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/solitaire.c b/apps/plugins/solitaire.c index aa5f876c80..abe2143ef8 100644 --- a/apps/plugins/solitaire.c +++ b/apps/plugins/solitaire.c @@ -722,7 +722,7 @@ CONFIG_KEYPAD == MROBE500_PAD # define HK_CUR2STACK "HOLD SELECT" # define HK_REM2STACK "VOL+" -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD # define SOL_QUIT BUTTON_POWER #elif CONFIG_KEYPAD == RG_NANO_PAD diff --git a/apps/plugins/spacerocks.c b/apps/plugins/spacerocks.c index 611a373418..95338d0ef9 100644 --- a/apps/plugins/spacerocks.c +++ b/apps/plugins/spacerocks.c @@ -362,7 +362,7 @@ #define AST_RIGHT BUTTON_RIGHT #define AST_FIRE BUTTON_PLAY -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/star.c b/apps/plugins/star.c index 735fb3bc60..86e95fc2ce 100644 --- a/apps/plugins/star.c +++ b/apps/plugins/star.c @@ -650,7 +650,7 @@ #define STAR_LEVEL_DOWN_NAME "VOL-" #define STAR_LEVEL_REPEAT_NAME "BACK" -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define STAR_QUIT BUTTON_POWER #define STAR_QUIT_NAME "POWER" diff --git a/apps/plugins/sudoku/sudoku.h b/apps/plugins/sudoku/sudoku.h index c43a6b0d96..8926bd68af 100644 --- a/apps/plugins/sudoku/sudoku.h +++ b/apps/plugins/sudoku/sudoku.h @@ -458,7 +458,7 @@ #define SUDOKU_BUTTON_TOGGLE BUTTON_PLAY #define SUDOKU_BUTTON_POSSIBLE BUTTON_BACK -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == RG_NANO_PAD diff --git a/apps/plugins/test_touchscreen.c b/apps/plugins/test_touchscreen.c index 3c9e559a45..b5999b672a 100644 --- a/apps/plugins/test_touchscreen.c +++ b/apps/plugins/test_touchscreen.c @@ -36,7 +36,7 @@ #elif CONFIG_KEYPAD == CREATIVE_ZENXFI2_PAD #define TOUCHSCREEN_QUIT BUTTON_POWER #define TOUCHSCREEN_TOGGLE BUTTON_MENU -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define TOUCHSCREEN_QUIT BUTTON_POWER #define TOUCHSCREEN_TOGGLE BUTTON_PLAY #elif (CONFIG_KEYPAD == ANDROID_PAD) diff --git a/apps/plugins/text_viewer/tv_button.h b/apps/plugins/text_viewer/tv_button.h index d1bacb1f10..e64ba00e58 100644 --- a/apps/plugins/text_viewer/tv_button.h +++ b/apps/plugins/text_viewer/tv_button.h @@ -560,7 +560,7 @@ #define TV_LINE_DOWN BUTTON_SCROLL_FWD #define TV_BOOKMARK BUTTON_PLAY -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD #define TV_BOOKMARK BUTTON_PLAY #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/vu_meter.c b/apps/plugins/vu_meter.c index e1178fc691..22f5fe4b31 100644 --- a/apps/plugins/vu_meter.c +++ b/apps/plugins/vu_meter.c @@ -443,7 +443,7 @@ #define LABEL_MENU "MENU" #define LABEL_VOLUME "VOL+/VOL-" -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/apps/plugins/wormlet.c b/apps/plugins/wormlet.c index fec98315af..af1e0c9662 100644 --- a/apps/plugins/wormlet.c +++ b/apps/plugins/wormlet.c @@ -396,7 +396,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define BTN_QUIT BUTTON_POWER #define BTN_STOPRESET BUTTON_BACK -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD @@ -525,7 +525,7 @@ CONFIG_KEYPAD == MROBE500_PAD #elif ((LCD_WIDTH == 240) && (LCD_HEIGHT == 240)) || \ ((LCD_WIDTH == 320) && (LCD_HEIGHT == 240)) || \ ((LCD_WIDTH == 240) && ((LCD_HEIGHT == 320) || (LCD_HEIGHT == 400))) || \ - ((LCD_WIDTH == 360) && (LCD_HEIGHT == 400)) + ((LCD_WIDTH == 360) && ((LCD_HEIGHT == 400) || (LCD_HEIGHT == 640))) #define FOOD_SIZE 7 #define ARGH_SIZE 8 #define SPEED 4 diff --git a/apps/plugins/xobox.c b/apps/plugins/xobox.c index 445ca77b18..12c9c4fd2b 100644 --- a/apps/plugins/xobox.c +++ b/apps/plugins/xobox.c @@ -350,7 +350,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define DOWN BUTTON_DOWN #define PAUSE BUTTON_PLAY -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == RG_NANO_PAD diff --git a/apps/plugins/zxbox/keymaps.h b/apps/plugins/zxbox/keymaps.h index fa7394abfa..e87c6afff3 100644 --- a/apps/plugins/zxbox/keymaps.h +++ b/apps/plugins/zxbox/keymaps.h @@ -282,7 +282,7 @@ #define ZX_UP BUTTON_UP #define ZX_DOWN BUTTON_DOWN -#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD +#elif CONFIG_KEYPAD == SHANLING_Q1_PAD || CONFIG_KEYPAD == HIBY_R3PROII_PAD || CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD /* use touchscreen */ #elif CONFIG_KEYPAD == MA_PAD diff --git a/backdrops/cabbiev2.360x640x16.bmp b/backdrops/cabbiev2.360x640x16.bmp new file mode 100644 index 0000000000000000000000000000000000000000..4b527dbf88d901ab5ba4f66a71812c88b7514e18 GIT binary patch literal 691254 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
  • 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%0Dk)&-0%UG%h=-mP$x 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/dev/null"); #endif fflush(stdout); mount_storage(true); diff --git a/docs/CREDITS b/docs/CREDITS index b7acbb46d6..9b64d7dfbc 100644 --- a/docs/CREDITS +++ b/docs/CREDITS @@ -765,6 +765,7 @@ Adam N. Burke Lê Khánh Bình Michael McAllister Hemant Kumar +Jerry Shen The libmad team The wavpack team diff --git a/firmware/SOURCES b/firmware/SOURCES index 3b3a62765d..7ae34427e9 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -187,7 +187,18 @@ target/hosted/hiby/hibylinux_codec.c #endif #endif -#if (defined(HIBY_R1) || defined(HIBY_R3PROII)) && !defined(SIMULATOR) +#if defined(HIDIZS_AP80MAX) && !defined(SIMULATOR) +target/hosted/hiby/usb-hiby-gadget.c +target/hosted/hiby/ap80max/button-ap80max.c +target/hosted/hiby/ap80max/powermgmt-ap80max.c +target/hosted/hiby/debug-hiby.c +#ifndef BOOTLOADER +target/hosted/hiby/ap80max/led-ap80max.c +target/hosted/hiby/hibylinux_codec.c +#endif +#endif + +#if (defined(HIBY_R1) || defined(HIBY_R3PROII) || defined(HIDIZS_AP80MAX)) && !defined(SIMULATOR) target/hosted/hiby/usb-dac-hiby.c #endif @@ -222,7 +233,7 @@ target/hosted/xduoo/xduoolinux_codec.c target/hosted/button-devinput.c #endif -#if defined(HIBY_LINUX) && !defined(HIBY_R3PROII) && !defined(HIBY_R1) && !defined(SIMULATOR) +#if defined(HIBY_LINUX) && !defined(HIBY_R3PROII) && !defined(HIBY_R1) && !defined(HIDIZS_AP80MAX) && !defined(SIMULATOR) target/hosted/usb-hiby.c #endif diff --git a/firmware/export/config.h b/firmware/export/config.h index 997d14e010..afa0b274f8 100644 --- a/firmware/export/config.h +++ b/firmware/export/config.h @@ -157,6 +157,7 @@ #define RG_NANO_PAD 77 #define CTRU_PAD 78 #define HIBY_R3PROII_PAD 79 +#define HIDIZS_AP80MAX_PAD 80 /* CONFIG_REMOTE_KEYPAD */ #define H100_REMOTE 1 @@ -580,6 +581,8 @@ Lyre prototype 1 */ #include "config/hibyr3proii.h" #elif defined(HIBY_R1) #include "config/hibyr1.h" +#elif defined(HIDIZS_AP80MAX) +#include "config/hidizsap80max.h" #else #error "unknown hardware platform!" #endif diff --git a/firmware/export/config/hidizsap80max.h b/firmware/export/config/hidizsap80max.h new file mode 100644 index 0000000000..38d8b49d31 --- /dev/null +++ b/firmware/export/config/hidizsap80max.h @@ -0,0 +1,105 @@ +/* + * This config file is for the Hidizs AP80 Pro Max based on the x1600E soc + */ + +/* For Rolo and boot loader */ +#define MODEL_NUMBER 125 +#define MODEL_NAME "HIDIZS AP80 PRO MAX" + +#define PIVOT_ROOT "/data/mnt/sd_0" +#define MULTIDRIVE_DIR "/data/mnt/usb" + +/* LCD dimensions. The framebuffer is not the panel: /dev/fb0 is 480x640 but + only the center 360 columns are scanned out - see hiby/lcd-target.h. + sqrt(360^2 + 640^2) / 2.95" = 249 */ +#define LCD_WIDTH 360 +#define LCD_HEIGHT 640 +#define LCD_DPI 249 + +#define HAVE_LCD_SLEEP +#define LCD_SLEEP_TIMEOUT (2*HZ) + +#define LCD_DEPTH 16 /* 65536 colours */ +#define LCD_PIXELFORMAT RGB565 /* rgb565 */ + +/* /dev/fb0 is 480 columns wide but only the center 360 display */ +#define FB_STRIDE_MISMATCH 480 +#define FB_STRIDE_XOFFSET ((FB_STRIDE_MISMATCH - LCD_WIDTH) / 2) + +#define CPU_FREQ 1008000000 + +#ifndef SIMULATOR +#define HAVE_GENERAL_PURPOSE_LED +#endif + +/* define this if you have access to the quickscreen */ +#define HAVE_QUICKSCREEN +#define HAVE_HOTKEY + +#define HAVE_HEADPHONE_DETECTION + +#ifndef BOOTLOADER +#define HAVE_BUTTON_DATA +#define HAVE_TOUCHSCREEN +#endif + +/* Identity, as the digitiser already reports in panel coordinates. EVIOCGABS + declares twice that range but the hardware does not use it */ +#define DEFAULT_TOUCHSCREEN_CALIBRATION { .A=1, .B=0, .C=0, \ + .D=0, .E=1, .F=0, \ + .divider=1 } + +#define HAVE_SCROLLWHEEL + +#ifndef CONFIG_BACKLIGHT_FADING +#undef CONFIG_BACKLIGHT_FADING +#endif + +/* KeyPad configuration for plugins */ +#define CONFIG_KEYPAD HIDIZS_AP80MAX_PAD + +/* There are no volume buttons, so HAVE_VOLUME_IN_LIST is not defined - the + scroll wheel handles volume instead */ + +/* Battery */ +#define BATTERY_TYPES_COUNT 1 + +/* Audio codec */ +#define HAVE_HIBY_LINUX_CODEC + +/* We don't have hardware controls */ +#define HAVE_SW_TONE_CONTROLS + +/* HW codec is flexible */ +#define HW_SAMPR_CAPS SAMPR_CAP_ALL_192 + +/* Battery */ +#define CONFIG_BATTERY_MEASURE (VOLTAGE_MEASURE|PERCENTAGE_MEASURE|TIME_MEASURE) + +#define BATTERY_CAPACITY_DEFAULT 100 /* default battery capacity */ +#define BATTERY_CAPACITY_MIN 100 /* min. capacity selectable */ +#define BATTERY_CAPACITY_MAX 100 /* max. capacity selectable */ +#define BATTERY_CAPACITY_INC 0 /* capacity increment */ + +/* Special backlight paths */ +#define BACKLIGHT_HIBY + +#define MIN_BRIGHTNESS_SETTING 1 +#define MAX_BRIGHTNESS_SETTING 100 +#define BRIGHTNESS_STEP 5 +#define DEFAULT_BRIGHTNESS_SETTING 100 + +/* ROLO */ +#define BOOTFILE_EXT "ap80max" +#define BOOTFILE "rockbox." BOOTFILE_EXT +#define BOOTDIR "/.rockbox" + +/* USB */ +#define HAVE_USB_ADB +#define HAVE_HOST_USB_AUDIO +#define HAVE_USB_POWER +#define USB_VID_STR "C502" +#define USB_PID_STR "008C" + +/* Generic HiBy stuff */ +#include "hibylinux.h" diff --git a/firmware/target/hosted/hiby/ap80max/button-ap80max.c b/firmware/target/hosted/hiby/ap80max/button-ap80max.c new file mode 100644 index 0000000000..1d61d4371d --- /dev/null +++ b/firmware/target/hosted/hiby/ap80max/button-ap80max.c @@ -0,0 +1,83 @@ +/*************************************************************************** + * __________ __ ___ + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2017 Marcin Bukat + * Copyright (C) 2025 by Melissa Autumn + * Copyright (C) 2026 by Jerry Shen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include + +#include "sysfs.h" +#include "button.h" +#include "button-target.h" +#include "hibylinux_codec.h" +#include "touchscreen.h" +#ifdef HAVE_BACKLIGHT +#include "backlight.h" +#endif /* HAVE_BACKLIGHT */ + +/* + * /dev/input: gpio keys, adc keys, ring encoder, touchscreen + */ + +int button_map(int keycode) +{ + switch(keycode) + { + case KEY_PLAYPAUSE: + return BUTTON_PLAY; + /* Keys are reversed as per glyphs */ + case KEY_PREVIOUSSONG: + return BUTTON_NEXT; + case KEY_NEXTSONG: + return BUTTON_PREV; + case KEY_POWER: + return BUTTON_POWER; + /* Scroll wheel */ + case KEY_LEFT: + return BUTTON_SCROLL_FWD; + case KEY_RIGHT: + return BUTTON_SCROLL_BACK; + + case BTN_TOUCH: + { +#ifdef HAVE_BACKLIGHT + if (is_backlight_on(true)) { + return BUTTON_TOUCH; + } + // Ignore + return 0; +#else + return BUTTON_TOUCH; +#endif + } + default: + return 0; + } +} + +bool headphones_inserted(void) +{ + #ifdef BOOTLOADER + int ps = 0; + #else + int ps = hiby_get_outputs(); + #endif + + return (ps == 2 || ps == 3); +} diff --git a/firmware/target/hosted/hiby/ap80max/button-target.h b/firmware/target/hosted/hiby/ap80max/button-target.h new file mode 100644 index 0000000000..3ee0aec152 --- /dev/null +++ b/firmware/target/hosted/hiby/ap80max/button-target.h @@ -0,0 +1,67 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Jerry Shen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef _BUTTON_TARGET_H_ +#define _BUTTON_TARGET_H_ + +#include +#include "config.h" + +/* Main unit's buttons - four keys and a scroll wheel, no volume keys. */ + +#define BUTTON_POWER 0x00000001 +#define BUTTON_PREV 0x00000002 +#define BUTTON_NEXT 0x00000004 +#define BUTTON_PLAY 0x00000008 + +/* Scroll wheel */ +#define BUTTON_SCROLL_BACK 0x00000010 +#define BUTTON_SCROLL_FWD 0x00000020 + +#define BUTTON_MAIN 0x0000003f + +#define BUTTON_TOUCH 0x00000040 + +/* Touchscreen virtual buttons */ +#define BUTTON_TOPLEFT 0x00001000 +#define BUTTON_TOPMIDDLE 0x00002000 +#define BUTTON_TOPRIGHT 0x00004000 +#define BUTTON_MIDLEFT 0x00008000 +#define BUTTON_CENTER 0x00010000 +#define BUTTON_MIDRIGHT 0x00020000 +#define BUTTON_BOTTOMLEFT 0x00040000 +#define BUTTON_BOTTOMMIDDLE 0x00080000 +#define BUTTON_BOTTOMRIGHT 0x00100000 + +/* Software power-off */ +#define POWEROFF_BUTTON BUTTON_POWER +#define POWEROFF_COUNT 25 + +/* For the simulator/sdl plugins/xworld */ +#ifndef BOOTLOADER +#define BUTTON_LEFT BUTTON_PREV +#define BUTTON_RIGHT BUTTON_NEXT +#endif + +/* The left/right aliases above don't exist in BOOTLOADER builds */ +#define NO_BUTTON_LR + +int button_map(int keycode); + +#endif /* _BUTTON_TARGET_H_ */ diff --git a/firmware/target/hosted/hiby/ap80max/led-ap80max.c b/firmware/target/hosted/hiby/ap80max/led-ap80max.c new file mode 100644 index 0000000000..a272557a3d --- /dev/null +++ b/firmware/target/hosted/hiby/ap80max/led-ap80max.c @@ -0,0 +1,117 @@ +/*************************************************************************** + * __________ __ ___ + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025 Melissa Autumn + * Copyright (C) 2026 by Jerry Shen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "backlight-target.h" +#include "sysfs.h" +#include "panic.h" +#include "lcd.h" +#include "settings.h" +#include "power.h" + +/* Same sgm31324 controller as the R3 Pro II, so the same sysfs node */ +static const char * const sysfs_led_type = +"/sys/class/leds/sgm31324-leds/led_pattern"; + +/* FIXME: taken from the R3 Pro II's leds_sgm31324_add.sh; this player ships + its own copy and the ordering has not been checked against it */ +enum AP80MAX_LED_PATTERNS { + LED_PATTERN_INVALID = -1, + LED_PATTERN_OFF = 0, + LED_PATTERN_LIGHT_BLUE, + LED_PATTERN_YELLOW, + LED_PATTERN_CYAN, + LED_PATTERN_ORANGE, + LED_PATTERN_UNK_1, /* This might do something, but it seems to just be off */ + LED_PATTERN_NORMAL_RED_BLINK, + LED_PATTERN_FAST_RED_BLINK, + LED_PATTERN_GREEN, + LED_PATTERN_BLUE, /* A slightly more blue blue */ + LED_PATTERN_PINK, + LED_PATTERN_WHITE, +}; + +int _last_buttonlight = LED_PATTERN_INVALID; + +void _set_pattern(enum AP80MAX_LED_PATTERNS pattern) { + if (_last_buttonlight != pattern) { + _last_buttonlight = pattern; + sysfs_set_int(sysfs_led_type, _last_buttonlight); + } +} + +#ifdef HAVE_GENERAL_PURPOSE_LED +void led_hw_brightness(int brightness) +{ + (void)brightness; +} + +void led_hw_charged(void) +{ + if (global_settings.use_led_indicators) { + _set_pattern(LED_PATTERN_GREEN); + } +} + +void led_hw_charging(void) +{ + _set_pattern(LED_PATTERN_NORMAL_RED_BLINK); +} + +void led_hw_off(void) +{ + // Don't turn the led off if charging. + if (charging_state()) + return; + + _set_pattern(LED_PATTERN_OFF); +} + +void led_hw_on(void) +{ + if (charging_state()) { + led_hw_charging(); + } else { + if (global_settings.use_led_indicators) { + _set_pattern(LED_PATTERN_LIGHT_BLUE); + } + else { + led_hw_off(); + } + } +} + +void led_hw_set_mode(bool mode) +{ + if (mode) { + led_hw_on(); + } + else { + led_hw_off(); + } +} +#endif diff --git a/firmware/target/hosted/hiby/ap80max/powermgmt-ap80max.c b/firmware/target/hosted/hiby/ap80max/powermgmt-ap80max.c new file mode 100644 index 0000000000..63e4255338 --- /dev/null +++ b/firmware/target/hosted/hiby/ap80max/powermgmt-ap80max.c @@ -0,0 +1,39 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2017 Marcin Bukat + * Copyright (C) 2025 by Melissa Autumn + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include "powermgmt.h" +#include "power.h" + +unsigned short battery_level_disksafe = 3470; + +/* the OF shuts down at this voltage */ +unsigned short battery_level_shutoff = 3400; + +/* voltages (millivolt) of 0%, 10%, ... 100% when charging disabled */ +unsigned short percent_to_volt_discharge[11] = +{ + 3400, 3675, 3715, 3750, 3775, 3810, 3850, 4025, 4120, 4230, 4350 +}; + +/* voltages (millivolt) of 0%, 10%, ... 100% when charging enabled */ +unsigned short percent_to_volt_charge[11] = +{ + 3485, 3780, 3836, 3857, 3890, 3930, 3986, 4025, 4120, 4230, 4350 +}; diff --git a/firmware/target/hosted/hiby/lcd-target.h b/firmware/target/hosted/hiby/lcd-target.h index 4882fab60a..0dab04cc69 100644 --- a/firmware/target/hosted/hiby/lcd-target.h +++ b/firmware/target/hosted/hiby/lcd-target.h @@ -26,7 +26,13 @@ #define LCD_OPTIMIZED_UPDATE_RECT extern fb_data *framebuffer; + +#ifdef FB_STRIDE_MISMATCH +#define LCD_FRAMEBUF_ADDR(col, row) \ + (framebuffer + (row)*FB_STRIDE_MISMATCH + (col) + FB_STRIDE_XOFFSET) +#else #define LCD_FRAMEBUF_ADDR(col, row) (framebuffer + (row)*LCD_WIDTH + (col)) +#endif extern void lcd_set_active(bool active); #endif /* __LCD_TARGET_H__ */ diff --git a/firmware/target/hosted/lcd-linuxfb.c b/firmware/target/hosted/lcd-linuxfb.c index e25dd37b21..63661a1254 100644 --- a/firmware/target/hosted/lcd-linuxfb.c +++ b/firmware/target/hosted/lcd-linuxfb.c @@ -106,6 +106,10 @@ void lcd_init_device(void) vinfo.yres, vinfo.bits_per_pixel); } #endif +#elif defined(FB_STRIDE_MISMATCH) + /* For framebuffers wider than the panel and cannot be resized */ + if (vinfo.bits_per_pixel != LCD_DEPTH) + panicf("Unexpected framebuffer depth: %d", vinfo.bits_per_pixel); #else /* Make sure we match our desired bitdepth */ if (vinfo.bits_per_pixel != LCD_DEPTH || vinfo.xres != LCD_WIDTH || vinfo.yres != LCD_HEIGHT) { @@ -167,6 +171,20 @@ void lcd_enable(bool on) extern void lcd_copy_buffer_rect(fb_data *dst, const fb_data *src, int width, int height); +#ifdef FB_STRIDE_MISMATCH +/* lcd_copy_buffer_rect() for framebuffers wider than the panel */ +static void fb_copy_rect(fb_data *dst, const fb_data *src, + int width, int height) +{ + while (height--) + { + memcpy(dst, src, width * sizeof(fb_data)); + dst += FB_STRIDE_MISMATCH; + src += LCD_WIDTH; + } +} +#endif + void lcd_update(void) { if (fd < 0) return; @@ -176,9 +194,15 @@ void lcd_update(void) #endif { /* Copy the Rockbox framebuffer to the second framebuffer */ +#ifdef FB_STRIDE_MISMATCH + /* Strides differ - not one contiguous run */ + fb_copy_rect(LCD_FRAMEBUF_ADDR(0, 0), FBADDR(0, 0), + LCD_WIDTH, LCD_HEIGHT); +#else fb_data *dst = LCD_FRAMEBUF_ADDR(0, 0) + (fb_plane * FRAMEBUFFER_SIZE); lcd_copy_buffer_rect(dst, FBADDR(0,0), LCD_WIDTH*LCD_HEIGHT, 1); +#endif redraw(); } } @@ -191,6 +215,10 @@ void lcd_update_rect(int x, int y, int width, int height) if (lcd_active()) #endif { +#ifdef FB_STRIDE_MISMATCH + /* Strides differ - do line-by-line */ + fb_copy_rect(LCD_FRAMEBUF_ADDR(x, y), FBADDR(x, y), width, height); +#else fb_data *dst = LCD_FRAMEBUF_ADDR(x, y) + (fb_plane * FRAMEBUFFER_SIZE); fb_data *src = FBADDR(x,y); @@ -205,6 +233,7 @@ void lcd_update_rect(int x, int y, int width, int height) /* Full width - copy as one line */ lcd_copy_buffer_rect(dst, src, LCD_WIDTH*height, 1); } +#endif redraw(); } } diff --git a/firmware/target/hosted/sdl/sim-ui-defines.h b/firmware/target/hosted/sdl/sim-ui-defines.h index 3a10589da6..5a08b27adf 100644 --- a/firmware/target/hosted/sdl/sim-ui-defines.h +++ b/firmware/target/hosted/sdl/sim-ui-defines.h @@ -525,6 +525,13 @@ #define UI_LCD_POSX 77 #define UI_LCD_POSY 74 +#elif defined(HIDIZS_AP80MAX) +#define UI_TITLE "Hidizs AP80 Pro Max" +#define UI_WIDTH 580 +#define UI_HEIGHT 880 +#define UI_LCD_POSX 100 +#define UI_LCD_POSY 100 + #elif defined(SIMULATOR) #error no UI defines #endif diff --git a/firmware/usb.c b/firmware/usb.c index c44a7df7fc..b41facb39d 100644 --- a/firmware/usb.c +++ b/firmware/usb.c @@ -50,7 +50,7 @@ #include "gui/skin_engine/skin_engine.h" #endif -#if defined(HIBY_R3PROII) || defined(HIBY_R1) +#if defined(HIBY_R3PROII) || defined(HIBY_R1) || defined(HIDIZS_AP80MAX) #include "usb-hiby-gadget.h" #endif #if defined(DX50) || defined(DX90) @@ -91,7 +91,7 @@ void usb_set_mode(int mode) usb_mode = mode; #if defined(DX50) || defined(DX90) ibasso_set_usb_mode(mode); -#elif defined(HIBY_R3PROII) || defined(HIBY_R1) +#elif defined(HIBY_R3PROII) || defined(HIBY_R1) || defined(HIDIZS_AP80MAX) hiby_set_usb_mode(mode); #endif } diff --git a/tools/builds.pm b/tools/builds.pm index 0aafee756f..08dd865593 100644 --- a/tools/builds.pm +++ b/tools/builds.pm @@ -471,6 +471,10 @@ $releasenotes="/wiki/ReleaseNotes400"; name => 'Hiby R3Pro II', status => 2, }, + 'hidizsap80max' => { + name => 'Hidizs AP80 Pro Max', + status => 1, + }, ); sub manualname { diff --git a/tools/configure b/tools/configure index 608f783763..bc43bf184b 100755 --- a/tools/configure +++ b/tools/configure @@ -1737,8 +1737,8 @@ cat <6h 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 literal 0 HcmV?d00001 diff --git a/uisimulator/buttonmap/SOURCES b/uisimulator/buttonmap/SOURCES index 4efbdee874..b9fa3b64ac 100644 --- a/uisimulator/buttonmap/SOURCES +++ b/uisimulator/buttonmap/SOURCES @@ -87,5 +87,7 @@ shanling-q1.c rg-nano.c #elif CONFIG_KEYPAD == HIBY_R3PROII_PAD hiby-r3proii.c +#elif CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD +hidizs-ap80max.c #endif #endif /* SIMULATOR */ diff --git a/uisimulator/buttonmap/hidizs-ap80max.c b/uisimulator/buttonmap/hidizs-ap80max.c new file mode 100644 index 0000000000..295267bdb0 --- /dev/null +++ b/uisimulator/buttonmap/hidizs-ap80max.c @@ -0,0 +1,57 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2021 by Solomon Peachy + * Copyright (C) 2026 by Jerry Shen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + + +#include +#include "button.h" +#include "buttonmap.h" + +int key_to_button(int keyboard_button) +{ + switch (keyboard_button) + { + case SDLK_UP: + return BUTTON_SCROLL_BACK; + case SDLK_DOWN: + return BUTTON_SCROLL_FWD; + case SDLK_LEFT: + return BUTTON_PREV; + case SDLK_RIGHT: + return BUTTON_NEXT; + case SDLK_RETURN: + case SDLK_SPACE: + return BUTTON_PLAY; + case SDLK_ESCAPE: + return BUTTON_POWER; + default: + return BUTTON_NONE; + } +} + +struct button_map bm[] = { + { SDLK_ESCAPE, 525, 270, 35, "Power" }, + { SDLK_LEFT, 526, 432, 25, "Previous" }, + { SDLK_SPACE, 526, 515, 25, "Play" }, + { SDLK_RIGHT, 526, 598, 25, "Next" }, + { SDLK_UP, 525, 230, 35, "Scroll Fwd" }, + { SDLK_DOWN, 525, 310, 35, "Scroll Back" }, + { 0, 0, 0, 0, "None" } +}; diff --git a/wps/WPSLIST b/wps/WPSLIST index 3e0ba230e0..d9438bb77b 100644 --- a/wps/WPSLIST +++ b/wps/WPSLIST @@ -85,6 +85,7 @@ wps.480x720x(16|24|32): cabbiev2.480x720x16.touch.wps wps.800x480x(16|24|32): cabbiev2.800x480x16.wps wps.480x800x(16|24|32): cabbiev2.480x800x16.wps wps.400x240x(16|24|32): cabbiev2.400x240x16.wps +wps.360x640x(16|24|32): cabbiev2.360x640x16.wps wps.360x400x(16|24|32): cabbiev2.360x400x16.wps wps.320x480x(16|24|32): cabbiev2.320x480x16.wps wps.320x240x(16|24|32): cabbiev2.320x240x16.wps @@ -118,6 +119,7 @@ Font.480x720x(16|24|32): 35-Adobe-Helvetica.fnt Font.800x480x(16|24|32): 35-Adobe-Helvetica.fnt Font.480x800x(16|24|32): 35-Adobe-Helvetica.fnt Font.400x240x(16|24|32): 15-Adobe-Helvetica.fnt +Font.360x640x(16|24|32): 35-Adobe-Helvetica.fnt Font.360x400x(16|24|32): 18-Adobe-Helvetica.fnt Font.320x480x(16|24|32): 27-Adobe-Helvetica.fnt Font.320x240x(16|24|32): 15-Adobe-Helvetica.fnt @@ -155,6 +157,7 @@ backdrop.480x720x(16|24|32): backdrops/cabbiev2.480x720x16.bmp backdrop.800x480x(16|24|32): backdrops/cabbiev2.800x480x16.bmp backdrop.480x800x(16|24|32): backdrops/cabbiev2.480x800x16.bmp backdrop.400x240x(16|24|32): backdrops/cabbiev2.400x240x16.bmp +backdrop.360x640x(16|24|32): backdrops/cabbiev2.360x640x16.bmp backdrop.360x400x(16|24|32): backdrops/cabbiev2.360x400x16.bmp backdrop.320x480x(16|24|32): backdrops/cabbiev2.320x480x16.bmp backdrop.320x240x(16|24|32): backdrops/cabbiev2.320x240x16.bmp @@ -185,6 +188,7 @@ iconset.480x720x(16|24|32): icons/tango_icons.32x32.bmp iconset.800x480x(16|24|32): icons/tango_icons.32x32.bmp iconset.480x800x(16|24|32): icons/tango_icons.32x32.bmp iconset.400x240x(16|24|32): icons/tango_icons.16x16.bmp +iconset.360x640x(16|24|32): icons/tango_icons.32x32.bmp iconset.360x400x(16|24|32): icons/tango_icons.32x32.bmp iconset.320x480x(16|24|32): icons/tango_icons.24x24.bmp iconset.320x240x(16|24|32): icons/tango_icons.16x16.bmp @@ -208,6 +212,7 @@ viewers iconset.480x720x(16|24|32): icons/tango_icons_viewers.32x32.bmp viewers iconset.800x480x(16|24|32): icons/tango_icons_viewers.32x32.bmp viewers iconset.480x800x(16|24|32): icons/tango_icons_viewers.32x32.bmp viewers iconset.400x240x(16|24|32): icons/tango_icons_viewers.16x16.bmp +viewers iconset.360x640x(16|24|32): icons/tango_icons_viewers.32x32.bmp viewers iconset.360x400x(16|24|32): icons/tango_icons_viewers.32x32.bmp viewers iconset.320x480x(16|24|32): icons/tango_icons_viewers.24x24.bmp viewers iconset.320x240x(16|24|32): icons/tango_icons_viewers.16x16.bmp diff --git a/wps/cabbiev2.360x640x16.wps b/wps/cabbiev2.360x640x16.wps new file mode 100644 index 0000000000..b44ec434a3 --- /dev/null +++ b/wps/cabbiev2.360x640x16.wps @@ -0,0 +1,87 @@ +# Cabbie v2.0 +# (C) 2007-2012 The Authors (see /rockbox/wps/AUTHORS) +# Derived from "cabbie" (C) Yohann Misquitta +# +# Laid out against wpsbackdrop-360x640x16.bmp, whose fixed landmarks are: +# title bar x=30..329 y=6..27 +# progress trough x=30..329 y=516..537 +# status row y=605..633, with shuffle baked at x=209 and repeat at x=273 +# Everything below is anchored to those; the content area is y=28..515. +# +# Disable Status Bar +%wd +# +# Load Backdrop +%X(wpsbackdrop-360x640x16.bmp) +# +# Preload Images +%xl(A,lock-360x640x16.bmp,0,0,2) +%xl(B,battery-360x640x16.bmp,0,0,10) +%xl(C,volume-360x640x16.bmp,0,0,10) +%xl(D,shuffle-360x640x16.bmp,0,0) +%xl(E,repeat-360x640x16.bmp,0,0,4) +%xl(F,playmode-360x640x16.bmp,0,0,5) +# +# Album Art/Info Viewport Conditional +%?C<%Vd(a)|%Vd(b)> +# +# Progress Bar +# Backdrop trough occupies x=30..329, y=516..537. +%V(30,516,300,20,-) +%pb(0,0,-,20,pb-360x640x16.bmp) +# +# Hold +%V(16,605,38,29,-) +%?mh<%xd(Aa)|%xd(Ab)> +# +# Battery +%V(69,605,66,29,-) +%?bp<%?bc<%xd(Ba)|%xd(Bb)>|%?bl<|%xd(Bc)|%xd(Bd)|%xd(Be)|%xd(Bf)|%xd(Bg)|%xd(Bh)|%xd(Bi)|%xd(Bj)>> +# +# Volume +%V(147,605,50,29,-) +%?pv<%xd(Ca)|%xd(Cb)|%xd(Cc)|%xd(Cd)|%xd(Ce)|%xd(Cf)|%xd(Cg)|%xd(Ch)|%xd(Ci)|%xd(Cj)> +# +# Shuffle +%V(208,605,55,29,-) +%?ps<%xd(D)> +# +# Repeat +%V(273,605,27,29,-) +%?mm<|%xd(Ea)|%xd(Eb)|%xd(Ec)|%xd(Ed)> +# +# Playmode +%V(309,605,36,29,-) +%?mp<%xd(Fa)|%xd(Fb)|%xd(Fc)|%xd(Fd)|%xd(Fe)> +# +# Time Elapsed/Remaining +%V(30,542,300,36,-) +%al%pc%ac%?Sr<%pe %Sx(of) %pp|%pp %Sx(of) %pe>%ar%pr +# +# Album Art +%ax%Vl(a,40,45,280,280,-) +%Cl(0,0,280,280,c,c) +%Cd +# +# Track Info - Album Art +%ax%Vl(a,0,329,-,187,1) +%s%ac%?id<%id|%?d(1)<%d(1)|%(root%)>> +%s%ac%?it<%it|%fn> +%s%ac%?ia<%ia|%?iA<%iA|%?d(2)<%d(2)|%(root%)>>> + +%s%ac%Sx(Next:) %?Ia<%Ia|%?IA<%IA|%?D(2)<%D(2)|%(root%)>>> - %?It<%It|%Fn> +# +# Track Info - No Album Art +%Vl(b,0,96,-,420,1) +%s%ac%?id<%id|%?d(1)<%d(1)|%(root%)>> +%s%ac%?it<%it|%fn> +%s%ac%?ia<%ia|%?iA<%iA|%?d(2)<%d(2)|%(root%)>>> +%ac%?iy<%iy|> + +%ac%?ig<%ig|> +%ac%?fv<%(vbr%) |>%fb kbit/s %fc + +%ac%Sx(Next:) +%s%ac%?Id<%Id|%?D(1)<%D(1)|%(root%)>> +%ac%s%?It<%It|%Fn> +%s%ac%?Ia<%Ia|%?IA<%IA|%?D(2)<%D(2)|%(root%)>>> diff --git a/wps/cabbiev2/battery-360x640x16.bmp b/wps/cabbiev2/battery-360x640x16.bmp new file mode 100644 index 0000000000000000000000000000000000000000..d00b4c1c5d584a04498c7c7d0bbca35934c19a9c GIT binary patch literal 58054 zcmeI530M?Yw#Uy!<3mj*^BnUMmvMBm%$PTMCK@%#XpBw-62~NpSxk%(Ok83T*NjeL zqB9y9XG~l`gNZtd>`Oxf%?^SJvNj-%%_dtnLZfT~0wUk{PIbYh?doc}8|be7rtkNs zICbu=>U+AX7k+g(wPc(^7U9ot2>^k|)P80$ZY=vl9cYZ72+zC(|2Ge2 z%m>F0V2O(n`m-zO(bBb`Panfl9=-7i(}hc_*Vn+KQ#5C-WzSql0t!j6jQHgeUumCm zh;OgJHwS#P8ToLP1wLwdXTg2S1aFScXMncS;XFrP>ELxmTizKYFb}+?Fms{S^!rxH zM_)t8RX83Chr;292skW(BlinE11J&>Q)vvQ`_bB*`Ufzg;0Q{>%<5laI2H}TdE~Q0 zeG}LEP1fuNe<`!Y(P;Sh73O&O=W>)s{5cB9Bljtv1nAUN5~x-{pTVv&%L080l950e z2`C_V7(C*b=h82meEz-G{QFj!I0Ev(Jr2%XBK~sXp2+A}-x7`CQUW<eH?^egowtO5ed<|HT_F5VEVCPY-?B<&)rXQ$UMPFFpY zTuVOO#m13Z@05A#>C9V1TnQnSb1&37rK*IW7n5tJ#VDu7D5pd#Cr2wMMJXpp6!u|N zI7vH+;)Tpxj+wU}Os?yTCTjzA0@Qo2(+McB+rZ#EiyO1ub*( zT1a>`MARtSV_HtjuCfjXc{>wFOyh>acDL&tjLJI>lru{zx=IaZjCv$@ zu%_p}@#-DoR+>)sA<#pG9_QRPoDUIKzf{=1tMtxXY3tZT)#xOZePZ3HgxZnuwIr|* z7O!N#C%Wu@GPm_WRo5FO9VAi-aV=k#ba*PdgrGIDI}qPO#EJ#-J2=C6vUcKmiTRE@ zVJbYP)&rx|K``HUU3O2;Z}X_R`_A<)CXR&Gz2#lLb=^YH7sXwW*ha+4g+(1WL&xGy z;*r!hajoAN$1_*k&#LZySaFv`)h1|a+e+`^d`P%*QE}(ly6)xpOrJ>B znt$IaartIO!JUAcJ!>nwiKGTDbO=p&JYFg4#2J2o&$MSs-^8_kSy$do z;_FGgO1(rwAX1UoxeET`2>hV(bU>s|!@A1b0Z?5}gYQrar?v09j8>@S@q0gWm*3j~6t!fQ6WYzFJ ztW$JPlintK&T|h1>ltN_pNC}Y5wi8@AjI9A>!JInUDV)qQ4ckaFyCv(Jj54=jqg&F zOW`gW)Tsf9HJxY=fOY8r8{ZWO-}Ovk2=%6a!t;MZk)v@|%6E^(03zo%*Rd~Yaxm0N z`L0I-1Q*;FhjsE@M4h6-`{J-pzWdW<2$O5X5oV=)7omB0aeo|cAy&$F&t*eoDUE~q zi!aOqUyQQ~z8h2ovDJFx z_?1=gT~XPH1m%dg*4yc71W&Vve`OVXH?7Vgx4~i0VFz7p!h{JJhgI<1jGIpRjZS-y zy!hga)22qU*k{AK!{7$ ze@E6IAR7;m&4-CK@Lkz$*Wy;!V`t~ho5#fQr+5D=qTCw!Zb|!&^7b8Pf*c(kF%0q# zk(Kh@%1$>$$AO?IXJ=<7j(>((Dc`NR>si<38I!VR%^D_-gP~T+ck8=-Z{GDy$@t=n zFWB?SO8IX6J)fK1-pT3PwrykLa1&W0-%aj-%bk$k3d=TvogIoC2)0tbn@~>@R76q* zp(PMn3{grO-|c6;nwVA#$7-!=jx^9Vd=LLId>7vlY%!5zJ^^y-EA>uSo18oXoOqhGaO&(v z=WDl}e-bTMH>8KGL07GXQ(tXyEp2o4igM*?*21Za+jmrUY!6P{!PBgTQ&)7j)pWVV zTyg7b2Q(V`tc6n}k5=9FOp$u>G;871s&3zgd%l_1e0iF+aB9^(pN4xr88RQ9W-Xlh zVkhKuGT%$oJeqGQd``{q=z1clCJc}6722%&JT^{UeUA?xYx%WS4P?`r!wlbpjZ+&| zHA7Fa4D8ZZECWkv@D$5nD`nW~W!UOvFoiR87Rx|qUzrSQSZ%$G;}?ie4mlDF&%dfm zOwzO-35QdE9|_*+WPbn`d>G;p4yQg6gNfaPibEKj+C2dacMK{HVQ^~vXStyKLBt^x zPJJ>J6T3~1Lol5BbOyuKO?{mUhf|-EVxBkIA85$Cla4Wh;ndz&vHA%4&eS-#p;?Gi zV}Twl#xay}ScX#vmDpi^v|BJQdp4%Sf}Hv?^5_N!zZ72fY)pp*Idz(vNBdvm!O^^K zp>gW$28Krmr%?b+c{CD3Y+OHj&@VJj&G6`US4kERj^=d>jZ+u3A&=gXn9qZwdEG+e z)D;~EkVmJ=SsaZl8V#06_W|nO-x`>j8H1-5es%~apIUj>v%cFi=QXZTZw1?@=12c3o%FsyMTm(cgN(?8Q2y z8&6Yatq)Qozcz(aUo)|y^A8g6&o(8qP2(BmGtJ4VkH#938O=9S{eC76<2kiyMV*f) zLU8^t9uE^Zbt1SMFD!S41b3heZ~f0ZUoSamK2B{|W;EYS_4}DG%I2!;Y+z=z{t5H- znpF4WbQYK(EER1?W;EYS_501pseKBVy4}O``p^;NuyJZEu^wkqX0(n;8sPfHaB*sr zGNX;5)BR(Il~bFP8Ep)m?jLI)rxux1*Lk4KXkHYpx5NCLx-T=DLT45<(^YD)eilsY z8N+rBic{Au$!v1XYcWm*17t??qG-Jx=I7Mv%;+5jOJ>b_deTBVoiDm8kG zH&=9OWk$2$nMKlQQ-j*W&8g!}s_Q&ZW;8E~*4trzPOT=J%8KJ`{oVt_c>oFmD@jPJK9< zN$<8u97D>fkHs_T-R|+`JseEEK2z>L$PcNHUtsRFXn{C}m{a2`9LuJdKaQd1)Th!R z^cun9>Sk_3&8g4k;0+8d5XVq+>ht-4cVs+n_T#}l{Db4vc>mStN@2t?C{7(%Vi%_v zaXhh~8C${{Jg2^N!$Eq>F+8`Q8C${{Jg3IA==>(9h&;V)c_=%FiBo4aEHAw699@9W znQas|vsjrfS-<8OwCP|io32uWZaz$$I={uWq|Nn$TrXQ5%Fbcp)R-1s(Xk_~%p~S) z%0|P-sWB~DT1i>69vH2si)GdpnrMSi@XA1e=e?{IKx~NECJu6%uhje$6-P~brQI% zbv)T3R9aUYgW}W{qS89z7#ydz6qV)%ViKo5lMQFG7$KDow*-~u#xXcfjZ|793@Xiy z%otAXCm1TNBMu`twJ@kOmxLS3sf9tMxg^{`P94&hPmNSM&#Y9MOSp%GQ)e|e7q+le znj&UOkdY~Ku`>2*I@LpT)buc947;mAaq8M78I7)a&8}vr(p#y;5ju?>6jYxQfV&BwsC43r>0C94K9PGy|jxO+%D>&hK*C(IJE>0 zOZrJP+L}6|(WTF}-A57st$!|3X|6a4)LN*TrcUcL%$`q;Nw`inZu>S z!tofwsnv1JojaF{j4k3ZnN#BpehFEyU;*XPTIiYjH5oK@Y7N#Lqs6A#P1y0YiyCx? zCUfeOOdLy=Ea4(!i+D`t)ap1^u3X7Q#uo9I%&FCJY}l}YvS}^!OjoI)iKjK4mgUsy zIKKSyOD;0Dh{t43{S)*0*t>Tx7a3c`V=|{!$FXP69xgJrh{t43jd6?^$tA^>^%&2o zA+`=g#b(Tv=2B^{$Hu8`oSHIaG`I|!_R=nDaJ#678a7UCY?6{_{gxn1vA6IJpFNm6u`+8ChIsr4u>Q{6T#zQB&QB60-8^4 zTA}J;*oxq7&5Y#KfyKSa)ND%iAeWd~juqWMKLoDd!m6FHay#IS7C!kq9E~^XQJAw` zB!g3FfK_>d|}DJbn7{61?C9^WK1$--1_`!>jMYtM92v_(#kwsoz@9=$*~* z-~YoK1a=@l$D=aJ9!8Ia2OoxMPs6`+^gSqdN?!RyTmcbf5F{rtRqXL_*$r`uhs0&0 z@K%SSa`2Yv)Av#OCTL*afe`RaRUdT77DXa4;8*o*LZtb4rfo%22D{eI?U?_W$JswQ5jc`~7TWI_$W zw?QV3@t5ixuBfJ8sV89-5Lh-RLE&)mhE6$oC9YVp!o|g9)22;ZOl@xjrs~|efzAmL zl{iVe$m-eY^^4P04<*-<4|lO~q}DrS-g-Lo77E6)59nMNt;d~f{R+ErQ5>i1#mA#TE7GyQPDQy~?tRna!%bZseGa6SFw9LtCA>q{! z(U(t+Db=Rx6G_gNt=qKbOIJ6Y4%#SAqgod)<9ncasSP+E5~i?^t$8B5>FMmIXL6dK zl{U}JX?{MlX+>`H%7WWtQtIKY4eB_4oz=XtsO{x~RuZ9rsGBQuZ|{(|4N}`eleg^r zE1kQhu{}D=z@0m0HsgFqSoKUL* z%dWBx2YEXaM@-{}!ge>zr&gy_Yu2IeEk&&^U9IUqz$RapcW649!6mKxGRyydS-v+Z zZ%=IIo~X1vk(c*IT-={u%k*B?9?$L{cftx+opal8K15voQepe9(mQjdtz#2aqmxwj ziFKnAYDdP`lE6adBb8}*UOk!HdZ4Q7jgk%$sRWu&t?2T!wARS(Kzs`kD;CJ_;0))< z+KJ~Sm^h93=JuFc4~$X=!NxH?zs;lO?mO4Jm^czz_m+40)^!U(UleyiVjB@F7Z!Ek z3>}L*iAPf3#I=6o@p$HH`&re!4=e7H$Qnqxv!~*&Pu)EsXj|!BoDT_CE-LOkTi3lD zpJ~ig(A|j3H!}+E1l;UdTiHz{HB2V8z=X%+m7-3Z;RpCkREgkk+bZ7qtx; zW5{5>{hU(uc&U0YjAg#n=p8|dHdgd;i>1~myf_6 zZ=?9Rv4e+?w$m%kF4JAc_bcqIWZBg}t$)qI!Fru^nN#{Wtry#;n=*y;HJ6bDu-O+use#+`MJ0)aZ_mapks!Vx3BY!-Msa_P(IuD z&$SoJ`S3X7&s1$X@B^J48k_!p(Diba?oBdkH|=BM#-(d!9e2!-@%Gj&{xMros%D3k?Y&qn?hO+ti+3i;w^=z^|w|>iy=eJAibdDK9-hRE~>%i!(JOBD^Oga*H zO4O;rl5(e|#7$;M$cEMbgqXdsau2N92Wt;OvJqo5aG3tND&w0IaPx6kn+cJ- zVR;%XYmDW)AZ8zI%)@2dp{08Mw!J=82PZFpE%~tO01G|_QF|cn2qqkZc3qxI$L541 zrofLU;JXZn-v4B*+z)YCxGoc#wy;I;w$r{VZY_Y=OjwoS1(Nb`L9Dk@Rz5CHgv2~p zo!JO$vLHSO5^`1ikPY7-ZbXPZ0y~T0xBQ9n*4lQ#)Dl>i4Qq}z!j=O3dLw?iTtysh z!O)06{3H{8wccAn`k+sm2U|}ii3?S+tB0EreyBw1i@Dg15_ z#ly+-fzIEVLo+F7_$$gA3vA-Q|6;q;l6EO)9MGB)*kXY$y;yVfH0{2hT+{ac8$bG+ zHK6RbINhlbXzbbCOzC0E!0p!7nzObNryC#ROpSB`aDMrFjV5C z`L-CibkkeLtIo7g<5irs!bbC?Lz-Dv_2z4OlOx8Pt6FAU!avQ1KCff=M4qtt_@@@3K7u|@kvoQ+xr2)n+)6nuXcBXVpT2`1f9tg3;RLCLpZ!HiHhRbhzknAq zD~6QZ#R;Ez>f*?wFQcJn@3qOh0l{q?*IbRUe`xPhw|kwsjRQuyTK8zKZReNFxBhtk zK2H6@6TY%)&(zWfx{NXiY~l&u4F{KOeD~tR7mrmz+sNE}t?^Sqr#Dxd=-onQ- zpSaqlW9y@pGt24%ezd?#d}Hj|ULD#xtMZd8b^hB%~ z>-G(@i1dSjU;fam@XoyQ`c{X^;MYAGR-dVduZ<4LzBZ@4enweckAmBNn~ikl`0bXU zwA1~Hf1&M6E2|4iErw1#pGn5axlKo06yMEKJN0ti*Om241-`7TA9wLl+ng%=`0vmx z*vaSlOtfl;qo(2Ku^5$tE4E`q6b>E>O?6d7XsD6R6#5K;uysb;UKCc_H64K-WptD3Y0VRXN^(Z~f-4Ar?F zfSAN@xXUC>krAuNT+|$e6scK<;_OM1z7uv7LF{4p$DsyL$+R{L)*V$r{qHjyR*6Y` z@>0J~IHqc9^hP7GBj$)x<}Bx}hK(kO&1!@Vd3=D&wy9Wm1QLyn2*f8Z@?}#KVP$Jh zavNd?>Nfar`3;AFAlP06>kY7uJ{fjMG(lvFh$ou)w6Eh_BiVDvi+m{pNGX=HoulZ< zF}Ys^oShM$Qu=|3jk(_AcOA#nf4H7g$M?|Ng^&J*l5Mz4O!%ur= ze$zbteDJnnM{GZhdrIM*8LUaw-kuA=J5D=twiT1FxOx?;@H84~NlCtY3XfUW3Jou! z(3}++W~gu|kZAahp`3p8SWw#RsA0J-N~sZs3gG*A3MgU<9oqm&SAvwqy40K<8OFs= zg#*HkmAGQ3RT1crUpG)4FxyL{!*`pY{3I^_J`0I(N8&vaZUGWQvx2rKVP+(F#S<68ij~ z<|O#5g0E`&*zk4xS#%iHMGZBQtdu-xg$wyo1OTm8E8}ZfP`;fqV^-CQDc*yEbeDjW z<_4yh^fKLe#e7q@t3;Pt>=9Cm+Gke}DX0zDS4t6BQ%%UG$qH;jQ+BCU6gmyQGTU_Q zWWL$MQtQP-Ev;|x!7BrcZbRrKj=*_xrdmaabI%^UMq~Mp-c+)HppawLgHGRtk?--) z6KmJCOMY$N!rRbysGzG6t)XRj%25dM1}H(BNdaxb(J2@1D^Ye|>W^0*`$_28du&|>A4{>}x zqNS8LMR{4VI4g^N@qSdNrY%|=yfWj`Bkh_D(E3;-x&DvS7md2`pm)(-`IJnM8Z@mZ z$l5HULa5Rd9kIBEWatr-|4?#oZ0SQ~=_a=igEd;5IJZyXoynITDPP6gZOKQgW!M>O zv#bw;R%o4u!tDHjo}vvq|KP*2y5N6Z26{4N|InRE)4}8XHk;lqefU9Hop0;`@l$Q{ zwwDtELS5{`wiThr`Zh1MGjT2zH-D+9fB$k_&lCS^vh$Q?_1=KwyiTSY;iZqL;u&=A zJ|6_%8`vA?K_(iF)`uWXrYW+Ni#%=bsP;ScRF-Y+ZmHE@s#{P|zo@c)VMYD)vbz4o zcm0xbV4%zT-`7{e(L^K9lZU=OT3-!;7x>SYr|71`owW#oH^#yTi(yU-{jS5v_n|{K IcU*w~1JZa&=>Px# literal 0 HcmV?d00001 diff --git a/wps/cabbiev2/pb-360x640x16.bmp b/wps/cabbiev2/pb-360x640x16.bmp new file mode 100644 index 0000000000000000000000000000000000000000..6b5884d059c928210cd3fd07e405f44dce8837e5 GIT binary patch literal 6654 zcmeHMc~F#P7=ON1JU~EQC8ZFdR1m_8#RC<0S@Bq{z#|h2H8qb!O$AdFG{pnO^~}tp za@0vP!>d!BF-pK1%So*qG>;-dN5?ToOZ)Y_@B8fmlFs-?GtGRz<$d4h_&vY(`JUN7 zb{AX!znhXozKJM;t!Ry?5uX~>Q3>7~`l+>+ZLt`jdV!#b4$u;kq4h}vX(mwn9;lhM z8A0!#K*W*Dh%Ua0=wsIqU2+Z7JQ8)6?uDbI0(rmP!RD*?u;~h~3SQ;+Ks^S)cgHDY zoV|sO9N$oW7aM-(RdyE{=WemK6{g*1F{t!5oMnH(d5PD>KjFN) z$bF?~y}uMuyu$V{$I=s~f^%HM{p?`=mJ^`&zGq+D^#kagZ$TSNSeH4wPl7fVgWmcI zw62h!?f>W%+`L!Hd@fm3OC!qX~LDqpVZD*a2IKP#( z)*t4YPdRr0v}_+}DX+Y}pjP}oSh5G&hd=YPa8KKqyNT~u&wA?)g5LZBTGqSJwj77H zi=Xj>lf1ZJO#=m*L6H`IAEiLOwP@w4)oW@RsAd9dR&m$o{=_^am zr7}Eql>|60k~%yoFTSTzvUIjM+;y&dTm(_g$)PU}89aEQCH3mvr>~=5e>a{{ z*0b(FRpU7h=%lJCZ(q9RYYD~N$F8qan~d}`u6GFD=EHfx9$l&tTwC7mdGy! ztFSm`x7n>$tF&!w$e7^S64_x_79`s2Y$P&SfDI9ryWnuS)y~T!E$LRVGgOAv?81~G zE@!s%WJgvZDP7Tl5ND77gre$!Yh{}ieHTf^5d@V}R4F6)98}7ZAt>r46{NhnLrS?cq?js} zLh=qTo)mU%lIL9^?o>f}r)p4K>$y($ppyhu*H(o>%f*&<^wX_c@@G*BX%^Z%EIjL8z-1E2%hMo<~kqdDXJ%_S}ZxE2mP3V;)~vx+f3Ld6KS-INedVp||7B*Fz6I z!o$O!3T-C7;}A6tdaOy);NZs%R6Bv-;HFKU2x=@xMdTk4SieC-v)SCpK-CGD%?%sW z4-D|vM@8gQt9G5bzJ7l73{;tbpPz5tI<;$ge(Mu6(@Zt0`2p1k)vRIS|L`dM16nT# AGynhq literal 0 HcmV?d00001 diff --git a/wps/cabbiev2/playmode-360x640x16.bmp b/wps/cabbiev2/playmode-360x640x16.bmp new file mode 100644 index 0000000000000000000000000000000000000000..148376b1278ab6c86ca90a85f96ae88b0c00013f GIT binary patch literal 15714 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 literal 0 HcmV?d00001 diff --git a/wps/cabbiev2/repeat-360x640x16.bmp b/wps/cabbiev2/repeat-360x640x16.bmp new file mode 100644 index 0000000000000000000000000000000000000000..f6fb08d909f824de13fa3ff1d03fa08a4b7d1833 GIT binary patch literal 9798 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 D3rvc- literal 0 HcmV?d00001 diff --git a/wps/cabbiev2/volume-360x640x16.bmp b/wps/cabbiev2/volume-360x640x16.bmp new file mode 100644 index 0000000000000000000000000000000000000000..7d8b8e40e8bcc135be48631e332213bd28bed264 GIT binary patch literal 44134 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 OxCviow3J&vH~AlZS+(H+ literal 0 HcmV?d00001 diff --git a/wps/cabbiev2/wpsbackdrop-360x640x16.bmp b/wps/cabbiev2/wpsbackdrop-360x640x16.bmp new file mode 100644 index 0000000000000000000000000000000000000000..a7ad1c6447db0d6c3f6a5802d7c09ef0187acf7b GIT binary patch literal 921670 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 literal 0 HcmV?d00001 From 9a7ffac2e4f46d812334d19fa7df59f21771eef2 Mon Sep 17 00:00:00 2001 From: Skye Date: Fri, 14 Aug 2026 22:39:13 +0900 Subject: [PATCH 083/408] pcm_sink: allow per-sink swvol/hwvol selection Change-Id: I5b0a66f2e41ca32a2d6c88bda3ffe0db0df08ce0 --- firmware/SOURCES | 4 ++-- firmware/export/audiohw.h | 8 +++++++ firmware/export/config.h | 8 +++++++ firmware/export/pcm-internal.h | 24 ++++++++----------- firmware/export/pcm_sink.h | 10 ++++++-- firmware/export/pcm_sw_volume.h | 10 ++++++-- firmware/pcm.c | 20 +++++++++++++--- firmware/pcm_sw_volume.c | 12 +++++----- firmware/target/arm/as3525/pcm-as3525.c | 1 + firmware/target/arm/imx233/pcm-imx233.c | 1 + .../arm/imx31/gigabeat-s/pcm-gigabeat-s.c | 1 + firmware/target/arm/pcm-telechips.c | 1 + firmware/target/arm/pp/pcm-pp.c | 1 + firmware/target/arm/rk27xx/pcm-rk27xx.c | 1 + .../arm/s3c2440/gigabeat-fx/pcm-meg-fx.c | 1 + firmware/target/arm/s5l8700/pcm-s5l8700.c | 1 + firmware/target/arm/s5l8702/pcm-s5l8702.c | 1 + .../arm/stm32/echoplayer/pcm-echoplayer.c | 1 + .../arm/tms320dm320/mrobe-500/pcm-mr500.c | 1 + .../sansa-connect/pcm-sansaconnect.c | 1 + firmware/target/coldfire/pcm-coldfire.c | 1 + firmware/target/hosted/android/pcm-android.c | 2 ++ firmware/target/hosted/ctru/pcm-ctru.c | 1 + firmware/target/hosted/ibasso/pcm-ibasso.c | 2 ++ firmware/target/hosted/pcm-alsa.c | 1 + firmware/target/hosted/sdl/pcm-sdl.c | 1 + .../target/mips/ingenic_jz47xx/pcm-jz4740.c | 1 + .../target/mips/ingenic_jz47xx/pcm-jz4760.c | 1 + .../target/mips/ingenic_x1000/pcm-x1000.c | 1 + firmware/usbstack/iap/audio.c | 1 + 30 files changed, 91 insertions(+), 29 deletions(-) diff --git a/firmware/SOURCES b/firmware/SOURCES index 7ae34427e9..488f61ccab 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -552,9 +552,9 @@ sound.c pcm_sampr.c pcm.c pcm_mixer.c -#ifdef HAVE_SW_VOLUME_CONTROL +#ifdef WANT_SWVOL pcm_sw_volume.c -#endif /* HAVE_SW_VOLUME_CONTROL */ +#endif /* WANT_SWVOL */ #ifdef HAVE_RECORDING enc_base.c #endif /* HAVE_RECORDING */ diff --git a/firmware/export/audiohw.h b/firmware/export/audiohw.h index c6308b3aff..75e65e05a4 100644 --- a/firmware/export/audiohw.h +++ b/firmware/export/audiohw.h @@ -696,4 +696,12 @@ AUDIOHW_SETTING(STEREO_WIDTH, "%", 0, 5, 0, 250, 100) # define PCM_NATIVE_BITDEPTH 16 #endif +#ifndef PCM_NATIVE_VOLUME_TYPE +#if !defined(HAVE_SW_VOLUME_CONTROL) || defined(PCM_SW_VOLUME_UNBUFFERED) +#define PCM_NATIVE_VOLUME_TYPE PCM_SINK_HWVOL +#else +#define PCM_NATIVE_VOLUME_TYPE PCM_SINK_SWVOL +#endif +#endif + #endif /* _AUDIOHW_H_ */ diff --git a/firmware/export/config.h b/firmware/export/config.h index afa0b274f8..1a70ad7bdb 100644 --- a/firmware/export/config.h +++ b/firmware/export/config.h @@ -1494,4 +1494,12 @@ Lyre prototype 1 */ #define ucschar_t unsigned short #endif +#ifdef HAVE_SW_VOLUME_CONTROL +#define WANT_SWVOL +#endif + +#if defined(PCM_NATIVE_BITDEPTH) && (PCM_NATIVE_BITDEPTH > 16) +#define WANT_SWVOL_32 +#endif + #endif /* __CONFIG_H__ */ diff --git a/firmware/export/pcm-internal.h b/firmware/export/pcm-internal.h index 4f69f77806..7ffbb08b36 100644 --- a/firmware/export/pcm-internal.h +++ b/firmware/export/pcm-internal.h @@ -45,21 +45,22 @@ #define PCM_FACTOR_UNITY (1u << PCM_SW_VOLUME_FRACBITS) #endif /* PCM_SW_VOLUME_FRACBITS */ +#ifdef HAVE_SDL_AUDIO #ifdef PCM_SW_VOLUME_UNBUFFERED /* Copies buffer with volume scaling applied */ void pcm_sw_volume_copy_buffer(void *dst, const void *src, size_t size); #define pcm_copy_buffer pcm_sw_volume_copy_buffer -#else /* !PCM_SW_VOLUME_UNBUFFERED */ -#ifdef HAVE_SDL_AUDIO +#else /* PCM_SW_VOLUME_UNBUFFERED */ #define pcm_copy_buffer memcpy -#endif +#endif /* PCM_SW_VOLUME_UNBUFFERED */ +#endif /* HAVE_SDL_AUDIO */ + #ifndef PCM_PLAY_DBL_BUF_SAMPLES #define PCM_PLAY_DBL_BUF_SAMPLES 1024 /* Max 4KByte chunks */ #endif #ifndef PCM_DBL_BUF_BSS #define PCM_DBL_BUF_BSS /* In DRAM, uncached may be better */ #endif -#endif /* PCM_SW_VOLUME_UNBUFFERED */ void pcm_sync_pcm_factors(void); #endif /* HAVE_SW_VOLUME_CONTROL */ @@ -126,20 +127,15 @@ static FORCE_INLINE enum pcm_dma_status pcm_play_call_status_cb( static FORCE_INLINE enum pcm_dma_status pcm_play_dma_status_callback(enum pcm_dma_status status) { -#if defined(HAVE_SW_VOLUME_CONTROL) && !defined(PCM_SW_VOLUME_UNBUFFERED) +#ifdef WANT_SWVOL extern enum pcm_dma_status - pcm_play_dma_status_callback_int(enum pcm_dma_status status); - return pcm_play_dma_status_callback_int(status); -#else + pcm_play_dma_status_callback_int_swvol(enum pcm_dma_status status); + if (pcm_current_sink_caps()->volume_type == PCM_SINK_SWVOL) + return pcm_play_dma_status_callback_int_swvol(status); +#endif /* WANT_SWVOL */ return pcm_play_call_status_cb(status); -#endif /* HAVE_SW_VOLUME_CONTROL && !PCM_SW_VOLUME_UNBUFFERED */ } -#if defined(HAVE_SW_VOLUME_CONTROL) && !defined(PCM_SW_VOLUME_UNBUFFERED) -void pcm_play_dma_start_int(const void *addr, size_t size); -void pcm_play_dma_stop_int(void); -#endif /* HAVE_SW_VOLUME_CONTROL && !PCM_SW_VOLUME_UNBUFFERED */ - /* Called by the bottom layer ISR when more data is needed. Returns true * if a new buffer is available, false otherwise. */ bool pcm_play_dma_complete_callback(enum pcm_dma_status status, diff --git a/firmware/export/pcm_sink.h b/firmware/export/pcm_sink.h index 522a5aa853..54eb261014 100644 --- a/firmware/export/pcm_sink.h +++ b/firmware/export/pcm_sink.h @@ -23,10 +23,16 @@ #include "config.h" +#define PCM_SINK_HWVOL 0 +#ifdef WANT_SWVOL +#define PCM_SINK_SWVOL 1 +#endif + struct pcm_sink_caps { const unsigned long* samprs; - uint16_t num_samprs; - uint16_t default_freq; + uint8_t num_samprs; + uint8_t default_freq; + uint8_t volume_type; }; struct pcm_sink_ops { diff --git a/firmware/export/pcm_sw_volume.h b/firmware/export/pcm_sw_volume.h index 5aba259b2a..dc9962fbb5 100644 --- a/firmware/export/pcm_sw_volume.h +++ b/firmware/export/pcm_sw_volume.h @@ -26,10 +26,11 @@ ** the library or a hardware necessity. Normally, it shouldn't be used and ** only the port developer can properly decide. **/ -#ifdef HAVE_SW_VOLUME_CONTROL +#ifdef WANT_SWVOL #include #include +#include #define PCM_MUTE_LEVEL INT_MIN @@ -41,6 +42,11 @@ void pcm_set_prescaler(int prescale); /* Set the per-channel volume cut/gain for all PCM playback */ void pcm_set_master_volume(int vol_l, int vol_r); -#endif /* HAVE_SW_VOLUME_CONTROL */ +void pcm_play_dma_start_int_swvol(const void *addr, size_t size); +void pcm_play_dma_stop_int_swvol(void); +enum pcm_dma_status pcm_play_dma_status_callback_int_swvol(enum pcm_dma_status status); +bool pcm_play_dma_complete_callback_swvol(enum pcm_dma_status status, + const void **addr, size_t *size); +#endif /* WANT_SWVOL */ #endif /* PCM_SW_VOLUME_H */ diff --git a/firmware/pcm.c b/firmware/pcm.c index 5fa368911f..bea1edb376 100644 --- a/firmware/pcm.c +++ b/firmware/pcm.c @@ -30,6 +30,7 @@ #include "general.h" #include "pcm-internal.h" #include "pcm_mixer.h" +#include "pcm_sw_volume.h" /** * Aspects implemented in the target-specific portion: @@ -105,12 +106,15 @@ struct pcm_sink* pcm_get_current_sink(void) return sinks[cur_sink]; } -#if !defined(HAVE_SW_VOLUME_CONTROL) || defined(PCM_SW_VOLUME_UNBUFFERED) /** Standard hw volume/unbuffered control functions - otherwise, see ** pcm_sw_volume.c **/ static inline void pcm_play_dma_start_int(const void *addr, size_t size) { -#ifdef HAVE_SW_VOLUME_CONTROL +#ifdef WANT_SWVOL + if (sinks[cur_sink]->caps.volume_type == PCM_SINK_SWVOL) { + pcm_play_dma_start_int_swvol(addr, size); + return; + } /* Smoothed transition might not have happened so sync now */ pcm_sync_pcm_factors(); #endif @@ -119,12 +123,23 @@ static inline void pcm_play_dma_start_int(const void *addr, size_t size) static inline void pcm_play_dma_stop_int(void) { +#ifdef WANT_SWVOL + if (sinks[cur_sink]->caps.volume_type == PCM_SINK_SWVOL) { + pcm_play_dma_stop_int_swvol(); + return; + } +#endif sinks[cur_sink]->ops.stop(); } bool pcm_play_dma_complete_callback(enum pcm_dma_status status, const void **addr, size_t *size) { +#ifdef WANT_SWVOL + if (sinks[cur_sink]->caps.volume_type == PCM_SINK_SWVOL) { + return pcm_play_dma_complete_callback_swvol(status, addr, size); + } +#endif /* Check status callback first if error */ if (status < PCM_DMAST_OK) status = pcm_play_dma_status_callback(status); @@ -137,7 +152,6 @@ bool pcm_play_dma_complete_callback(enum pcm_dma_status status, pcm_play_stop_int(); return false; } -#endif /* !HAVE_SW_VOLUME_CONTROL || PCM_SW_VOLUME_UNBUFFERED */ void pcm_play_stop_int(void) { diff --git a/firmware/pcm_sw_volume.c b/firmware/pcm_sw_volume.c index 99e5d62cb4..293cda643b 100644 --- a/firmware/pcm_sw_volume.c +++ b/firmware/pcm_sw_volume.c @@ -210,7 +210,7 @@ void pcm_sync_pcm_factors(void) #endif } -#ifndef PCM_SW_VOLUME_UNBUFFERED +#ifdef WANT_SWVOL /* source buffer from client */ static const void * volatile src_buf_addr = NULL; static size_t volatile src_buf_rem = 0; @@ -227,7 +227,7 @@ static unsigned int frame_count, frame_err, frame_frac; /** Overrides of certain functions in pcm.c and pcm-internal.h **/ -bool pcm_play_dma_complete_callback(enum pcm_dma_status status, +bool pcm_play_dma_complete_callback_swvol(enum pcm_dma_status status, const void **addr, size_t *size) { /* Check status callback first if error */ @@ -268,7 +268,7 @@ static void update_frame_params(size_t size) /* Obtain the next buffer and prepare it for pcm driver playback */ enum pcm_dma_status -pcm_play_dma_status_callback_int(enum pcm_dma_status status) +pcm_play_dma_status_callback_int_swvol(enum pcm_dma_status status) { if (status != PCM_DMAST_STARTED) return status; @@ -327,7 +327,7 @@ static void start_pcm(bool reframe) pcm_get_current_sink()->ops.play(pcm_dbl_buf[1], pcm_dbl_buf_size[1]); } -void pcm_play_dma_start_int(const void *addr, size_t size) +void pcm_play_dma_start_int_swvol(const void *addr, size_t size) { src_buf_addr = addr; /* divide by 2 for 32 bit, optimize away to 1 for 16 bit */ @@ -335,14 +335,14 @@ void pcm_play_dma_start_int(const void *addr, size_t size) start_pcm(true); } -void pcm_play_dma_stop_int(void) +void pcm_play_dma_stop_int_swvol(void) { pcm_get_current_sink()->ops.stop(); src_buf_addr = NULL; src_buf_rem = 0; } -#endif /* PCM_SW_VOLUME_UNBUFFERED */ +#endif /* WANT_SWVOL */ /** Internal **/ diff --git a/firmware/target/arm/as3525/pcm-as3525.c b/firmware/target/arm/as3525/pcm-as3525.c index 029c075e52..dbc362910f 100644 --- a/firmware/target/arm/as3525/pcm-as3525.c +++ b/firmware/target/arm/as3525/pcm-as3525.c @@ -205,6 +205,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/arm/imx233/pcm-imx233.c b/firmware/target/arm/imx233/pcm-imx233.c index 0c5b4624f4..7ee5860c0f 100644 --- a/firmware/target/arm/imx233/pcm-imx233.c +++ b/firmware/target/arm/imx233/pcm-imx233.c @@ -175,6 +175,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = audiohw_preinit, diff --git a/firmware/target/arm/imx31/gigabeat-s/pcm-gigabeat-s.c b/firmware/target/arm/imx31/gigabeat-s/pcm-gigabeat-s.c index 25d149f74d..772964aaae 100644 --- a/firmware/target/arm/imx31/gigabeat-s/pcm-gigabeat-s.c +++ b/firmware/target/arm/imx31/gigabeat-s/pcm-gigabeat-s.c @@ -229,6 +229,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/arm/pcm-telechips.c b/firmware/target/arm/pcm-telechips.c index 3c9ca6b9b5..37c69a5323 100644 --- a/firmware/target/arm/pcm-telechips.c +++ b/firmware/target/arm/pcm-telechips.c @@ -174,6 +174,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/arm/pp/pcm-pp.c b/firmware/target/arm/pp/pcm-pp.c index 2a1ddd7668..08b7328e1c 100644 --- a/firmware/target/arm/pp/pcm-pp.c +++ b/firmware/target/arm/pp/pcm-pp.c @@ -517,6 +517,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/arm/rk27xx/pcm-rk27xx.c b/firmware/target/arm/rk27xx/pcm-rk27xx.c index 3d7532d787..88139aecc4 100644 --- a/firmware/target/arm/rk27xx/pcm-rk27xx.c +++ b/firmware/target/arm/rk27xx/pcm-rk27xx.c @@ -263,6 +263,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/arm/s3c2440/gigabeat-fx/pcm-meg-fx.c b/firmware/target/arm/s3c2440/gigabeat-fx/pcm-meg-fx.c index 432862d39d..c76790d609 100644 --- a/firmware/target/arm/s3c2440/gigabeat-fx/pcm-meg-fx.c +++ b/firmware/target/arm/s3c2440/gigabeat-fx/pcm-meg-fx.c @@ -220,6 +220,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/arm/s5l8700/pcm-s5l8700.c b/firmware/target/arm/s5l8700/pcm-s5l8700.c index 5bd2d4046d..8e36c9d60e 100644 --- a/firmware/target/arm/s5l8700/pcm-s5l8700.c +++ b/firmware/target/arm/s5l8700/pcm-s5l8700.c @@ -272,6 +272,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/arm/s5l8702/pcm-s5l8702.c b/firmware/target/arm/s5l8702/pcm-s5l8702.c index bd110f31c4..9af30b9c4b 100644 --- a/firmware/target/arm/s5l8702/pcm-s5l8702.c +++ b/firmware/target/arm/s5l8702/pcm-s5l8702.c @@ -220,6 +220,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/arm/stm32/echoplayer/pcm-echoplayer.c b/firmware/target/arm/stm32/echoplayer/pcm-echoplayer.c index d4944ce241..d1b4830d47 100644 --- a/firmware/target/arm/stm32/echoplayer/pcm-echoplayer.c +++ b/firmware/target/arm/stm32/echoplayer/pcm-echoplayer.c @@ -309,6 +309,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/arm/tms320dm320/mrobe-500/pcm-mr500.c b/firmware/target/arm/tms320dm320/mrobe-500/pcm-mr500.c index 8cb966fb8a..f8c261755e 100644 --- a/firmware/target/arm/tms320dm320/mrobe-500/pcm-mr500.c +++ b/firmware/target/arm/tms320dm320/mrobe-500/pcm-mr500.c @@ -162,6 +162,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/arm/tms320dm320/sansa-connect/pcm-sansaconnect.c b/firmware/target/arm/tms320dm320/sansa-connect/pcm-sansaconnect.c index 0f529f733d..61cd143221 100644 --- a/firmware/target/arm/tms320dm320/sansa-connect/pcm-sansaconnect.c +++ b/firmware/target/arm/tms320dm320/sansa-connect/pcm-sansaconnect.c @@ -164,6 +164,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/coldfire/pcm-coldfire.c b/firmware/target/coldfire/pcm-coldfire.c index 531feeec2d..24ef8ee190 100644 --- a/firmware/target/coldfire/pcm-coldfire.c +++ b/firmware/target/coldfire/pcm-coldfire.c @@ -306,6 +306,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/hosted/android/pcm-android.c b/firmware/target/hosted/android/pcm-android.c index 467f60e6cb..ca53d9a27d 100644 --- a/firmware/target/hosted/android/pcm-android.c +++ b/firmware/target/hosted/android/pcm-android.c @@ -29,6 +29,7 @@ #include "pcm-internal.h" #include "pcm_sampr.h" #include "pcm_sink.h" +#include "audiohw.h" extern JNIEnv *env_ptr; @@ -220,6 +221,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/hosted/ctru/pcm-ctru.c b/firmware/target/hosted/ctru/pcm-ctru.c index 4c296b6284..999b87ca0e 100644 --- a/firmware/target/hosted/ctru/pcm-ctru.c +++ b/firmware/target/hosted/ctru/pcm-ctru.c @@ -364,6 +364,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/hosted/ibasso/pcm-ibasso.c b/firmware/target/hosted/ibasso/pcm-ibasso.c index ea5651ac7e..94f9359029 100644 --- a/firmware/target/hosted/ibasso/pcm-ibasso.c +++ b/firmware/target/hosted/ibasso/pcm-ibasso.c @@ -33,6 +33,7 @@ #include "pcm-internal.h" #include "pcm_sampr.h" #include "pcm_sink.h" +#include "audiohw.h" #include "sound/asound.h" #include "tinyalsa/asoundlib.h" @@ -458,6 +459,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/hosted/pcm-alsa.c b/firmware/target/hosted/pcm-alsa.c index 7e4833fdfe..10b379b03d 100644 --- a/firmware/target/hosted/pcm-alsa.c +++ b/firmware/target/hosted/pcm-alsa.c @@ -775,6 +775,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/hosted/sdl/pcm-sdl.c b/firmware/target/hosted/sdl/pcm-sdl.c index 2b9df50faf..d2ed462a4d 100644 --- a/firmware/target/hosted/sdl/pcm-sdl.c +++ b/firmware/target/hosted/sdl/pcm-sdl.c @@ -449,6 +449,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/mips/ingenic_jz47xx/pcm-jz4740.c b/firmware/target/mips/ingenic_jz47xx/pcm-jz4740.c index a843480309..93fc4f4ca6 100644 --- a/firmware/target/mips/ingenic_jz47xx/pcm-jz4740.c +++ b/firmware/target/mips/ingenic_jz47xx/pcm-jz4740.c @@ -193,6 +193,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/mips/ingenic_jz47xx/pcm-jz4760.c b/firmware/target/mips/ingenic_jz47xx/pcm-jz4760.c index 37718d08da..29e35b63a4 100644 --- a/firmware/target/mips/ingenic_jz47xx/pcm-jz4760.c +++ b/firmware/target/mips/ingenic_jz47xx/pcm-jz4760.c @@ -178,6 +178,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/target/mips/ingenic_x1000/pcm-x1000.c b/firmware/target/mips/ingenic_x1000/pcm-x1000.c index 24dc49a1a1..05baa3cf44 100644 --- a/firmware/target/mips/ingenic_x1000/pcm-x1000.c +++ b/firmware/target/mips/ingenic_x1000/pcm-x1000.c @@ -208,6 +208,7 @@ struct pcm_sink builtin_pcm_sink = { .samprs = hw_freq_sampr, .num_samprs = HW_NUM_FREQ, .default_freq = HW_FREQ_DEFAULT, + .volume_type = PCM_NATIVE_VOLUME_TYPE, }, .ops = { .init = sink_dma_init, diff --git a/firmware/usbstack/iap/audio.c b/firmware/usbstack/iap/audio.c index 9dcbd01025..f4706b56d6 100644 --- a/firmware/usbstack/iap/audio.c +++ b/firmware/usbstack/iap/audio.c @@ -187,6 +187,7 @@ struct pcm_sink iap_pcm_sink = { .samprs = samprs, .num_samprs = ARRAYLEN(samprs), .default_freq = 0, + .volume_type = PCM_SINK_HWVOL, }, .ops = { .init = sink_nop, From df85814e74541df1c8434d375384638aa4475933 Mon Sep 17 00:00:00 2001 From: Skye Date: Fri, 14 Aug 2026 22:39:35 +0900 Subject: [PATCH 084/408] erosqnative: use hwvol at runtime on es9018k2m Change-Id: I0ea89ff44a7d7c7c4aeb65ec2b56287324533bcc --- firmware/export/pcm_sink.h | 2 +- .../erosqnative/audiohw-erosqnative.c | 9 ++++---- .../target/mips/ingenic_x1000/pcm-x1000.c | 23 +++++++++++-------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/firmware/export/pcm_sink.h b/firmware/export/pcm_sink.h index 54eb261014..c78d4af617 100644 --- a/firmware/export/pcm_sink.h +++ b/firmware/export/pcm_sink.h @@ -47,7 +47,7 @@ struct pcm_sink_ops { struct pcm_sink { /* characteristics */ - const struct pcm_sink_caps caps; + struct pcm_sink_caps caps; /* operations */ const struct pcm_sink_ops ops; diff --git a/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c b/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c index 8bab20abfa..db3e2a305f 100644 --- a/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c +++ b/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c @@ -20,6 +20,7 @@ #include "system.h" #include "audiohw.h" +#include "pcm_sink.h" #include "pcm_sw_volume.h" #include "pcm_sampr.h" #include "i2c-target.h" @@ -102,11 +103,12 @@ void audiohw_init(void) } #endif es9018k2m_present_flag = true; + builtin_pcm_sink.caps.volume_type = PCM_SINK_HWVOL; /* Default is 32-bit data, and it works ok. Enabling the following * causes issue. Which is weird, I definitely thought AIC was configured - * for 24-bit data... */ - // es9018k2m_write_reg(ES9018K2M_REG1_INPUT_CONFIG, 0b01001100); // 24-bit data + * for 16-bit data... */ + // es9018k2m_write_reg(ES9018K2M_REG1_INPUT_CONFIG, 0b00001100); // 16-bit data /* Datasheet: Sets the number os FSR edges that must occur before * * the DPLL and ASRC can lock on to the the incoming Signal. * @@ -192,9 +194,6 @@ void audiohw_set_volume(int vol_l, int vol_r) l = l <= PCM5102A_VOLUME_MIN ? PCM_MUTE_LEVEL : l; r = r <= PCM5102A_VOLUME_MIN ? PCM_MUTE_LEVEL : r; - /* set software volume just below unity due to - * DAC offset. We don't want to overflow the PCM system. */ - pcm_set_master_volume(-1, -1); es9018k2m_set_volume_async(l, r); } else /* PCM5102A */ diff --git a/firmware/target/mips/ingenic_x1000/pcm-x1000.c b/firmware/target/mips/ingenic_x1000/pcm-x1000.c index 05baa3cf44..3fb616c7e9 100644 --- a/firmware/target/mips/ingenic_x1000/pcm-x1000.c +++ b/firmware/target/mips/ingenic_x1000/pcm-x1000.c @@ -70,16 +70,21 @@ static void sink_dma_init(void) audiohw_init(); #if (PCM_NATIVE_BITDEPTH > 16) - /* Program audio format (stereo, 24 bit samples) */ - jz_writef(AIC_CCR, PACK16(0), CHANNEL_V(STEREO), - OSS_V(24BIT), ISS_V(24BIT), M2S(0)); - jz_writef(AIC_I2SCR, SWLH(0)); -#else - /* Program audio format (stereo, packed 16 bit samples) */ - jz_writef(AIC_CCR, PACK16(1), CHANNEL_V(STEREO), - OSS_V(16BIT), ISS_V(16BIT), M2S(0)); - jz_writef(AIC_I2SCR, SWLH(0)); + if (builtin_pcm_sink.caps.volume_type != PCM_SINK_HWVOL) + { + /* Program audio format (stereo, 24 bit samples) */ + jz_writef(AIC_CCR, PACK16(0), CHANNEL_V(STEREO), + OSS_V(24BIT), ISS_V(24BIT), M2S(0)); + jz_writef(AIC_I2SCR, SWLH(0)); + } + else #endif + { + /* Program audio format (stereo, packed 16 bit samples) */ + jz_writef(AIC_CCR, PACK16(1), CHANNEL_V(STEREO), + OSS_V(16BIT), ISS_V(16BIT), M2S(0)); + jz_writef(AIC_I2SCR, SWLH(0)); + } /* Set DMA settings */ jz_writef(AIC_CFG, TFTH(16), RFTH(15)); From 9a80f5af55d9d58e7596a88b53d05a607ccdac92 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 14 Aug 2026 22:42:07 -0400 Subject: [PATCH 085/408] erosqnative: Updated documentation about an odd quirk of hw3/hw4 platforms The ES9018K2M DAC used on hw3/hw4 defaults to 32-bit audio frames but when configuring it to match the SoC I2S settings (ie 16 or 24-bit) it results in garbled output for unexplained reasons. Change-Id: If3074f38178b19520ca6222a1e020ea210069a6d --- .../mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c b/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c index db3e2a305f..43c85741f6 100644 --- a/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c +++ b/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c @@ -105,10 +105,11 @@ void audiohw_init(void) es9018k2m_present_flag = true; builtin_pcm_sink.caps.volume_type = PCM_SINK_HWVOL; - /* Default is 32-bit data, and it works ok. Enabling the following - * causes issue. Which is weird, I definitely thought AIC was configured - * for 16-bit data... */ - // es9018k2m_write_reg(ES9018K2M_REG1_INPUT_CONFIG, 0b00001100); // 16-bit data + /* ES9018K2M defaults to 32-bit sample depth, which appears to work + properly with both 16-bit and 24-bit AIC configuration! Enabling + the following (ie to match the actual AIC configuration) produces + garbled output! + // es9018k2m_write_reg(ES9018K2M_REG1_INPUT_CONFIG, 0b00001100); // 16-bit data /* Datasheet: Sets the number os FSR edges that must occur before * * the DPLL and ASRC can lock on to the the incoming Signal. * From 8f8a0e5230c44ce0628afb2b7811e6c378c771db Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 14 Aug 2026 22:45:14 -0400 Subject: [PATCH 086/408] swvol: Switch to using WANT_SWVOL_32 in pcm_sw_volume.c Change-Id: Id28c4f50144e60bc13fc834641b1a0a7491a36b8 --- firmware/pcm_sw_volume.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/pcm_sw_volume.c b/firmware/pcm_sw_volume.c index 293cda643b..d4ed29f1e9 100644 --- a/firmware/pcm_sw_volume.c +++ b/firmware/pcm_sw_volume.c @@ -49,7 +49,7 @@ static uint32_t pcm_factor_l = 0, pcm_factor_r = 0; static typeof (memcpy) *pcm_scaling_fn = NULL; /* take care of some defines for 32-bit software vol */ -#if (PCM_NATIVE_BITDEPTH > 16) /* >16-bit */ +#ifdef WANT_SWVOL_32 /* >16-bit */ # define HAVE_SWVOL_32 # define PCM_VOL_SAMPLE_SIZE (2 * sizeof (int32_t)) # define PCM_DBL_BUF_SIZE_T int32_t From 83d69483017667e935eb34aad72239142e95576c Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sat, 15 Aug 2026 07:33:02 -0400 Subject: [PATCH 087/408] Fix yellow introduced in 9a80f5af55 (Back-to-back series of comment blocks had a missing closure in the middle) Change-Id: I11f36687c6d35bea7cadd0e7975e3c897f68fd93 --- .../target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c b/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c index 43c85741f6..b80ef55d12 100644 --- a/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c +++ b/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c @@ -108,7 +108,7 @@ void audiohw_init(void) /* ES9018K2M defaults to 32-bit sample depth, which appears to work properly with both 16-bit and 24-bit AIC configuration! Enabling the following (ie to match the actual AIC configuration) produces - garbled output! + garbled output! */ // es9018k2m_write_reg(ES9018K2M_REG1_INPUT_CONFIG, 0b00001100); // 16-bit data /* Datasheet: Sets the number os FSR edges that must occur before * From 459c5e79374994d527ee4cc9fe1e58d898512787 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sat, 15 Aug 2026 07:36:10 -0400 Subject: [PATCH 088/408] Move Hidizs AP80MAX to the 'unstable' list This means it will end up in nightlies, get voice builds, etc. Next up is to add it to the themesite.. Change-Id: I78dc77addebceb49773adaa3b70e9a457585fe62 --- tools/builds.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/builds.pm b/tools/builds.pm index 08dd865593..3adf0f2e38 100644 --- a/tools/builds.pm +++ b/tools/builds.pm @@ -473,7 +473,7 @@ $releasenotes="/wiki/ReleaseNotes400"; }, 'hidizsap80max' => { name => 'Hidizs AP80 Pro Max', - status => 1, + status => 2, }, ); From b616047311294d30f14fd3afc0fe39e809e27e70 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Sat, 15 Aug 2026 20:23:49 +0100 Subject: [PATCH 089/408] usb-designware: fix ISO frame scheduling The frame-scheduling logic cherry picked from the 'rockpod' fork [1] was not correct for high-speed hosts and broke USB Audio output. The DWC seems to schedule based on the DWC_DSTS.SOFFN field, which is a 14-bit microframe number for high-speed hosts. For full speed hosts SOFFN is the LSB-aligned 11-bit frame number. For high-speed hosts, USB Audio and iAP use a bInterval of 4 for the ISO endpoint which means the host sends a packet every 8 microframes. Thus, we only receive data on even microframes, which is why adding frame scheduling broke things (and why we got away with not doing it before). For full speed the bInterval is 1, we get a packet each frame and we do need to tell the core to receive on odd frames. This code is still not completely correct for arbitrary ISO endpoints -- for that we would need the function drivers to provide the (micro)frame number on which they want to send or receive. Tested by forcing full speed at the device side by setting USB_DW_DCFG_SPEED=3. USB Audio works fine in both high and full speed modes now. [1] https://github.com/nuxcodes/rockpod/commit/c390dfdbdf1e63396700af222aa33747769cea15 Change-Id: I17d283821cd0861e414c48208ca67f6e98464d7c --- firmware/drivers/usb-designware.c | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/firmware/drivers/usb-designware.c b/firmware/drivers/usb-designware.c index d6030caceb..b6f1691176 100644 --- a/firmware/drivers/usb-designware.c +++ b/firmware/drivers/usb-designware.c @@ -730,8 +730,17 @@ static void usb_dw_epstart(int epnum, enum usb_dw_epdir epdir, DWC_EPTSIZ(epnum, epdir) = eptsiz; if (((DWC_EPCTL(epnum, epdir) >> 18) & 0x3) == EPTYP_ISOCHRONOUS) { - /* Schedule the transfer for the next frame. */ - if ((DWC_DSTS >> 8) & 1) + /* + * Handle frame scheduling for isochronous endpoints. + * + * TODO: this needs to take into account the endpoint's bInterval, + * and currently will not work for USB 2.0 endpoints that require + * one packet per microframe. For slower USB 2.0 endpoints data is + * only transferred on *even* frames; SETD1PIDOF is not used. + * This also breaks for USB 1.0 endpoints with bInterval > 1 for + * the same reason. + */ + if (usb_drv_port_speed() || (usb_drv_get_frame_number() & 1)) DWC_EPCTL(epnum, epdir) |= EPENA | nak | SETD0PIDEF; else DWC_EPCTL(epnum, epdir) |= EPENA | nak | SETD1PIDOF; @@ -1524,10 +1533,14 @@ int usb_drv_send(int endpoint, void *ptr, int length) return dw_ep->status; } -int usb_drv_get_frame_number() +int usb_drv_get_frame_number(void) { - // SOFFN is 14 bits, the least significant 3 appear to be some sort of microframe count. - // The USB spec says a frame number is 11 bits. This way we get 1 frame per millisecond, - // just like we're supposed to! - return (DWC_DSTS >> 11) & 0x7FF; + /* + * SOFFN is a 14-bit microframe number for high-speed hosts and + * a plain frame number for full-speed hosts. + */ + if (usb_drv_port_speed()) + return (DWC_DSTS >> 11) & 0x7FF; + else + return (DWC_DSTS >> 8) & 0x3FFF; } From cacbd9aad2b9b523ddeb04cd67fdf7c5adbec885 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Sun, 16 Aug 2026 14:09:11 +0200 Subject: [PATCH 090/408] skin_engine/quickscreen: get rid of unused code, update comment, naming - remove quickscreen_set_option prototype left behind from commit a22749b - make skin_load internal to the skin_engine - remove skin_load's isfile parameter, since it's always true - update comment for skin_data_load - slight naming adjustments - replace MAX_PATH with sizeof Change-Id: I6fcbcf0d3e36730c5096f38d31f8e44b92498068 --- apps/gui/quickscreen.h | 1 - apps/gui/skin_engine/skin_engine.c | 24 ++++++++++++------------ apps/gui/skin_engine/skin_engine.h | 2 -- apps/gui/skin_engine/skin_parser.c | 4 ++-- apps/gui/skin_engine/wps_internals.h | 6 ++---- 5 files changed, 16 insertions(+), 21 deletions(-) diff --git a/apps/gui/quickscreen.h b/apps/gui/quickscreen.h index 28a012669b..1631a62e02 100644 --- a/apps/gui/quickscreen.h +++ b/apps/gui/quickscreen.h @@ -45,7 +45,6 @@ enum quickscreen_return { }; extern int quick_screen_quick(int button_enter); -int quickscreen_set_option(void *data); bool is_setting_quickscreenable(const struct settings_list *setting); #endif /*_GUI_QUICK_SCREEN_H_*/ diff --git a/apps/gui/skin_engine/skin_engine.c b/apps/gui/skin_engine/skin_engine.c index 61a0c65e02..b7aaf65a6b 100644 --- a/apps/gui/skin_engine/skin_engine.c +++ b/apps/gui/skin_engine/skin_engine.c @@ -203,22 +203,22 @@ void settings_apply_skins(void) skin_backdrop_show(sb_get_backdrop(i)); } -void skin_load(enum skinnable_screens skin, enum screen_type screen, - const char *buf, bool isfile) +static void skin_load(enum skinnable_screens skin, enum screen_type screen, + const char *filename) { bool loaded = false; skin_helpers[skin]->process(screen, &skins[skin][screen].data, true); - if (buf && *buf) - loaded = skin_data_load(screen, &skins[skin][screen].data, buf, isfile, - &skins[skin][screen].stats); + if (filename && *filename) + loaded = skin_data_load(screen, &skins[skin][screen].data, + filename, true, &skins[skin][screen].stats); if (!loaded && skin_helpers[skin]->default_skin) { loaded = skin_data_load(screen, &skins[skin][screen].data, - skin_helpers[skin]->default_skin(screen), false, - &skins[skin][screen].stats); + skin_helpers[skin]->default_skin(screen), + false, &skins[skin][screen].stats); skins[skin][screen].failsafe_loaded = loaded; } @@ -230,8 +230,8 @@ void skin_load(enum skinnable_screens skin, enum screen_type screen, #endif } -static char* get_skin_filename(char *buf, size_t buf_size, - enum skinnable_screens skin, enum screen_type screen) +static char* get_skin_filename(enum skinnable_screens skin, enum screen_type screen, + char *buf, size_t buf_size) { (void)screen; char *setting = NULL, *ext = NULL; @@ -300,10 +300,10 @@ struct gui_wps *skin_get_gwps(enum skinnable_screens skin, enum screen_type scre if (skins[skin][screen].data.wps_loaded == false) { - char filename[MAX_PATH]; - char *buf = get_skin_filename(filename, MAX_PATH, skin, screen); + char buf[MAX_PATH]; + const char *filename = get_skin_filename(skin, screen, buf, sizeof buf); cpu_boost(true); - skin_load(skin, screen, buf, true); + skin_load(skin, screen, filename); cpu_boost(false); } return &skins[skin][screen].gui_wps; diff --git a/apps/gui/skin_engine/skin_engine.h b/apps/gui/skin_engine/skin_engine.h index f06c4c86f0..6fe3f7248b 100644 --- a/apps/gui/skin_engine/skin_engine.h +++ b/apps/gui/skin_engine/skin_engine.h @@ -73,8 +73,6 @@ void skin_backdrop_set_buffer(int backdrop_id, struct skin_viewport *svp); */ int skin_wait_for_action(enum skinnable_screens skin, int context, int timeout); -void skin_load(enum skinnable_screens skin, enum screen_type screen, - const char *buf, bool isfile); struct gui_wps *skin_get_gwps(enum skinnable_screens skin, enum screen_type screen); void gui_sync_skin_init(void); diff --git a/apps/gui/skin_engine/skin_parser.c b/apps/gui/skin_engine/skin_parser.c index af0c83a8b2..0bbabb9f3f 100644 --- a/apps/gui/skin_engine/skin_parser.c +++ b/apps/gui/skin_engine/skin_parser.c @@ -2591,8 +2591,8 @@ static int skin_element_callback(struct skin_element* element, void* data) return CALLBACK_OK; } -/* to setup up the wps-data from a format-buffer (isfile = false) - from a (wps-)file (isfile = true)*/ +/* Set up skin data from a format buffer (isfile = false) + or from skin file (isfile = true) */ bool skin_data_load(enum screen_type screen, struct wps_data *wps_data, const char *buf, bool isfile, struct skin_stats *stats) { diff --git a/apps/gui/skin_engine/wps_internals.h b/apps/gui/skin_engine/wps_internals.h index e39358b80d..092f0991b0 100644 --- a/apps/gui/skin_engine/wps_internals.h +++ b/apps/gui/skin_engine/wps_internals.h @@ -44,10 +44,8 @@ struct skin_stats *skin_get_stats(int number, int screen); #define skin_clear_stats(stats) memset(stats, 0, sizeof(struct skin_stats)) bool skin_backdrop_get_debug(int index, char **path, int *ref_count, size_t *size); -/* - * setup up the skin-data from a format-buffer (isfile = false) - * or from a skinfile (isfile = true) - */ +/* Set up skin data from a format buffer (isfile = false) + or from skin file (isfile = true) */ bool skin_data_load(enum screen_type screen, struct wps_data *wps_data, const char *buf, bool isfile, struct skin_stats *stats); From be62759005e1c2d343a02e9b39a2efa67b424f43 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Sun, 16 Aug 2026 15:08:13 +0200 Subject: [PATCH 091/408] quickscreen: fix MIN_LINES off-by-one Appears to be a regression introduced in e454a39, where MIN_LINES was increased from 4 to 5, along with changing nb_lines <= MIN_LINES to nb_lines < MIN_LINES The top and bottom sections are supposed to switch from 2 to 1 lines if there are fewer available lines than MIN_LINES. Each of the three vertical section uses two lines, resulting in overlapping viewports, if there are exactly 5 available lines. Icons mentioned in the comment don't really play into that. Change-Id: I3a0e921d9f53a490fece0ce59457feaf84a80d3b --- apps/gui/quickscreen.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/gui/quickscreen.c b/apps/gui/quickscreen.c index bc245dd80a..535ba23409 100644 --- a/apps/gui/quickscreen.c +++ b/apps/gui/quickscreen.c @@ -41,9 +41,9 @@ #include "appevents.h" #include "statusbar-skinned.h" - /* 1 top, 1 bottom, 2 on either side, 1 for the icons - * if enough space, top and bottom have 2 lines */ -#define MIN_LINES 5 + /* 2 lines for each of the three vertical sections (top/middle/bottom). + If less space is available, the top and bottom each lose a line. */ +#define MIN_LINES (3*2) #define MAX_NEEDED_LINES 10 /* pixels between the 2 center items minimum or between text and icons, * and between text and parent boundaries */ From 8cfa4bfd8cd596a48d2afea441f297f2519008fd Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Sun, 16 Aug 2026 15:18:04 +0200 Subject: [PATCH 092/408] quickscreen: update SBS after each button press Quickscreen will ignore update_delay so that the SBS is updated after each button press, without any lag. The SBS may display settings that are adjusted by the Quickscreen (e.g. cabbiev2's Shuffle and Repeat icons, or the brightness slider included in themes like Adwaitapod and Themify). Change-Id: I28716e81cb77e235eb009bff91c96f510365f435 --- apps/gui/quickscreen.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/gui/quickscreen.c b/apps/gui/quickscreen.c index 535ba23409..af42df6e65 100644 --- a/apps/gui/quickscreen.c +++ b/apps/gui/quickscreen.c @@ -416,14 +416,10 @@ static void quickscreen_run(struct quickscreen * qs) } else if (button == qs->button_enter) can_quit = true; - else if (button == ACTION_QS_VOLUP) { + else if (button == ACTION_QS_VOLUP) adjust_volume(1); - sb_skin_force_next_update(); - } - else if (button == ACTION_QS_VOLDOWN) { + else if (button == ACTION_QS_VOLDOWN) adjust_volume(-1); - sb_skin_force_next_update(); - } else if (button == ACTION_STD_CONTEXT) { qs->result |= QUICKSCREEN_GOTO_SHORTCUTS_MENU; @@ -434,6 +430,10 @@ static void quickscreen_run(struct quickscreen * qs) if (button == ACTION_STD_CANCEL) break; + + /* Ignore SBS update_delay, so that setting + changes are immediately visible. */ + sb_skin_force_next_update(); } /* Notify that we're exiting this screen */ cond_talk_ids_fq(VOICE_OK); From 0b52c2893378a4c6ae893f31994708a6e72832b4 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Fri, 14 Aug 2026 13:35:21 +0100 Subject: [PATCH 093/408] dircache: use alloca to avoid VLA in struct extension Clang does not support VLA-in-struct so use alloca to do the same thing by hand. Change-Id: Ib135d1a90b00aac3098e4c64f9f5f5e397fe771c --- firmware/common/dircache.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/firmware/common/dircache.c b/firmware/common/dircache.c index 4b584fca56..1464ffab79 100644 --- a/firmware/common/dircache.c +++ b/firmware/common/dircache.c @@ -1395,15 +1395,12 @@ static void sab_process_dir(struct file_base_info *infop, bool issab) should also filled in beforehand */ /* allocate the stack right now to the max demand */ - struct dirsab - { - struct sab sab; - struct sab_component stack[issab ? DIRCACHE_MAX_DEPTH : 1]; - } dirsab; - struct sab *sabp = &dirsab.sab; + size_t stackdepth = (issab ? DIRCACHE_MAX_DEPTH : 1); + struct sab *sabp = alloca(sizeof(*sabp) + + sizeof(sabp->stack[0]) * stackdepth); sabp->quit = false; - sabp->stackend = &sabp->stack[ARRAYLEN(dirsab.stack)]; + sabp->stackend = &sabp->stack[stackdepth]; sabp->top = sabp->stackend; sabp->info = *infop; From 2c5482cab938357d4eac436333b06f7082d306c4 Mon Sep 17 00:00:00 2001 From: neofright Date: Wed, 19 Aug 2026 18:30:48 +0100 Subject: [PATCH 094/408] splash: make iPod reFresh colour workaround pixel-format independent The splash contrast workaround for the iPod reFresh themes compares foreground and background colours against hard-coded RGB565 values. This prevents the workaround from being applied on targets using other pixel formats, such as XRGB8888 hosted targets. Use LCD_RGBPACK() and SCREEN_COLOR_TO_NATIVE() to obtain the equivalent colours for the active screen instead. Code change and commit message generated by GPT-5.6 Sol. Compiled on my hosted Eros Q build and confirmed working correctly. Change-Id: Id15669bc7b8cec11bd4ee645c0b671db4499d2cd --- apps/gui/splash.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/gui/splash.c b/apps/gui/splash.c index 8619d36265..fd680eac87 100644 --- a/apps/gui/splash.c +++ b/apps/gui/splash.c @@ -306,7 +306,10 @@ static bool splash_internal(struct screen * screen, const char *fmt, va_list ap, bg = screen->get_background(); broken = (fg == bg) || - (bg == 63422 && fg == 65535); /* -> iPod reFresh themes from '22 */ + (bg == SCREEN_COLOR_TO_NATIVE(screen, + LCD_RGBPACK(244, 244, 244)) && + fg == SCREEN_COLOR_TO_NATIVE(screen, + LCD_RGBPACK(255, 255, 255))); vp->drawmode = DRMODE_FG; /* can't do vp->fg_pattern here, since set_foreground does a bit more on From e3f622a85108bcfb4036bf36747890c8517959cd Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Thu, 20 Aug 2026 14:26:49 +0100 Subject: [PATCH 095/408] Fix red in 2c5482cab9 The error is caused by grayscale >1bpp targets which do not define LCD_RGBPACK(). Change-Id: Ifc995d720f11fe3df8ac534bd0777e162541dc54 --- apps/gui/splash.c | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/gui/splash.c b/apps/gui/splash.c index fd680eac87..440164984b 100644 --- a/apps/gui/splash.c +++ b/apps/gui/splash.c @@ -305,11 +305,18 @@ static bool splash_internal(struct screen * screen, const char *fmt, va_list ap, fg = screen->get_foreground(); bg = screen->get_background(); - broken = (fg == bg) || - (bg == SCREEN_COLOR_TO_NATIVE(screen, - LCD_RGBPACK(244, 244, 244)) && - fg == SCREEN_COLOR_TO_NATIVE(screen, - LCD_RGBPACK(255, 255, 255))); + if (fg == bg) + broken = true; + +#if defined(HAVE_LCD_COLOR) + /* iPod reFresh themes from '22 */ + if (screen->is_color) + { + if (bg == SCREEN_COLOR_TO_NATIVE(screen, LCD_RGBPACK(244, 244, 244)) && + fg == SCREEN_COLOR_TO_NATIVE(screen, LCD_RGBPACK(255, 255, 255))) + broken = true; + } +#endif vp->drawmode = DRMODE_FG; /* can't do vp->fg_pattern here, since set_foreground does a bit more on From b621a519e2a732cae75e2c76c5d045865da53f2d Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Tue, 18 Aug 2026 16:19:07 +0100 Subject: [PATCH 096/408] sdmmc_host: add helper to get bus frequency from enum Change-Id: I5796d19a4c28986bbb3674a39cc93f3fa473ae3c --- firmware/drivers/sdmmc_host.c | 11 +++++++++++ firmware/export/sdmmc_host.h | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/firmware/drivers/sdmmc_host.c b/firmware/drivers/sdmmc_host.c index eae852bcbf..06d1509573 100644 --- a/firmware/drivers/sdmmc_host.c +++ b/firmware/drivers/sdmmc_host.c @@ -144,6 +144,17 @@ void sdmmc_host_set_medium_present(struct sdmmc_host *host, bool present) } #endif +size_t sdmmc_host_get_bus_freq(uint32_t clock) +{ + switch (clock) + { + case SDMMC_BUS_CLOCK_400KHZ: return 400000; + case SDMMC_BUS_CLOCK_25MHZ: return 25000000; + case SDMMC_BUS_CLOCK_50MHZ: return 50000000; + default: return 0; + } +} + #ifdef CONFIG_STORAGE_MULTI static int sdmmc_host_count_drives(struct sdmmc_host **array, size_t array_size) diff --git a/firmware/export/sdmmc_host.h b/firmware/export/sdmmc_host.h index 30419faa0a..411d73299a 100644 --- a/firmware/export/sdmmc_host.h +++ b/firmware/export/sdmmc_host.h @@ -291,4 +291,10 @@ void sdmmc_host_init_medium_present(struct sdmmc_host *host, bool present) INIT_ */ void sdmmc_host_set_medium_present(struct sdmmc_host *host, bool present); +/** + * Returns clock frequency in Hz for an `SDMMC_BUS_CLOCK_*` constant. + * Returns zero if fed an invalid constant. + */ +size_t sdmmc_host_get_bus_freq(uint32_t clock); + #endif /* __SDMMC_HOST_H__ */ From f69ee23fa161b8e3df919bfb78594034a0266420 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Tue, 18 Aug 2026 16:19:21 +0100 Subject: [PATCH 097/408] stm32h7: sdmmc: use sdmmc_host bus frequency helper Change-Id: I6d58a95b08b8edb8a32088b3903403d84537ac12 --- firmware/target/arm/stm32/sdmmc-stm32h7.c | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/firmware/target/arm/stm32/sdmmc-stm32h7.c b/firmware/target/arm/stm32/sdmmc-stm32h7.c index cfd7fabbef..13fd606eb4 100644 --- a/firmware/target/arm/stm32/sdmmc-stm32h7.c +++ b/firmware/target/arm/stm32/sdmmc-stm32h7.c @@ -57,17 +57,6 @@ #define BUSY_END_BITS \ (BUSY_SUCCESS_BITS | DATA_ERROR_BITS) -static size_t get_sdmmc_bus_freq(uint32_t clock) -{ - switch (clock) - { - case SDMMC_BUS_CLOCK_400KHZ: return 400000; - case SDMMC_BUS_CLOCK_25MHZ: return 25000000; - case SDMMC_BUS_CLOCK_50MHZ: return 50000000; - default: return 0; - } -} - static size_t abs_diff(size_t a, size_t b) { return a > b ? a - b : b - a; @@ -208,7 +197,7 @@ void stm32h7_sdmmc_set_bus_clock(void *controller, uint32_t clock) return; size_t ker_freq = stm32_clock_get_frequency(ctl->clock); - size_t bus_freq = get_sdmmc_bus_freq(clock); + size_t bus_freq = sdmmc_host_get_bus_freq(clock); if (!bus_freq) panicf("%s", __func__); From 11817f99f80492ae9990e625828d7a161aea9928 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Fri, 14 Aug 2026 20:30:05 +0100 Subject: [PATCH 098/408] stm32h7: sdmmc: handle CMD12 before second cache discard Because DMA is still running until CMD12 is issued the second cache discard must occur after CMD12, otherwise the cache may contain a stale copy of data. While the buffer is garbage at this point it's probably not a good idea to leave the buffer in a weird state where the cache doesn't match what's in RAM. Change-Id: I7caf91d17631c92686ef0ad6f148e6c1d1bcbfa0 --- firmware/target/arm/stm32/sdmmc-stm32h7.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/firmware/target/arm/stm32/sdmmc-stm32h7.c b/firmware/target/arm/stm32/sdmmc-stm32h7.c index 13fd606eb4..ed93f229df 100644 --- a/firmware/target/arm/stm32/sdmmc-stm32h7.c +++ b/firmware/target/arm/stm32/sdmmc-stm32h7.c @@ -378,16 +378,8 @@ int stm32h7_sdmmc_submit_command(void *controller, /* Wait for command completion */ semaphore_wait(&ctl->sem, TIMEOUT_BLOCK); - /* - * Discard data from speculative reads that may have - * accessed the buffer during the DMA transfer. - */ + /* Save original command status code */ int cmd_error = ctl->cmd_error; - if (cmd_error == SDMMC_STATUS_OK) - { - if (SDMMC_DATA_DIR(cmd->flags) == SDMMC_DATA_READ) - discard_dcache_range(buff_addr, buff_size); - } /* * If a data transfer command fails we need to issue CMD12 @@ -409,6 +401,15 @@ int stm32h7_sdmmc_submit_command(void *controller, stm32h7_sdmmc_submit_command(ctl, &cmd12, NULL); } + /* + * Discard data from speculative reads that may have + * accessed the buffer during the DMA transfer. Must + * be done after the CMD12 above otherwise the cache + * may end up out of sync with main memory. + */ + if (SDMMC_DATA_DIR(cmd->flags) == SDMMC_DATA_READ) + discard_dcache_range(buff_addr, buff_size); + /* Return error from original command */ return cmd_error; } From 9039355bf833521d026b9f4f4e2dc3fde96f032d Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Wed, 19 Aug 2026 11:39:16 +0100 Subject: [PATCH 099/408] timeout: include for intptr_t Change-Id: I5c5bd4e68d970f344b99e318f7818e4773bdda40 --- firmware/kernel/include/timeout.h | 1 + 1 file changed, 1 insertion(+) diff --git a/firmware/kernel/include/timeout.h b/firmware/kernel/include/timeout.h index 1775000c52..140e9d5c9f 100644 --- a/firmware/kernel/include/timeout.h +++ b/firmware/kernel/include/timeout.h @@ -21,6 +21,7 @@ #define _KERNEL_H_ #include "config.h" +#include struct timeout; From 3d44fe94f3f3bf8834ced3e50de57fb395fea9df Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Mon, 17 Aug 2026 18:59:13 +0200 Subject: [PATCH 100/408] skin_display: simplify skin_wait_for_action Reduces GNU Complexity score from 6 to 2. Slightly adjusts behavior: 1) With TIMEOUT_NOBLOCK and peak meters enabled, skin_wait_for_action always returned BUTTON_NONE, and did nothing else. Presumably unintentional. WPS was not affected, since it always uses HZ/5 for the timeout. The FM screen uses TIMEOUT_NOBLOCK only when fms_do_button_loop is called with update_screen == true. skin_wait_for_action now returns the result of get_action in any given scenario. 2) fixes incorrect next_refresh calculation when peak_meters are enabled on multiple screens 3) button loop now refreshes peak meters when current_tick >= next_refresh, instead of when current_tick > next_refresh Change-Id: Iaca37dbdde6057f593659b2feb97b61ac4cc4b02 --- apps/gui/skin_engine/skin_display.c | 69 +++++++++++------------------ apps/gui/skin_engine/skin_engine.h | 5 +-- 2 files changed, 27 insertions(+), 47 deletions(-) diff --git a/apps/gui/skin_engine/skin_display.c b/apps/gui/skin_engine/skin_display.c index 027026cd8c..106fd6f85c 100644 --- a/apps/gui/skin_engine/skin_display.c +++ b/apps/gui/skin_engine/skin_display.c @@ -756,52 +756,35 @@ bool skin_has_sbs(struct gui_wps *gwps) return draw; } -/* do the button loop as often as required for the peak meters to update - * with a good refresh rate. - */ +/* Enter button loop updating peak meter at a high refresh rate */ int skin_wait_for_action(enum skinnable_screens skin, int context, int timeout) { - int button = ACTION_NONE; - /* when the peak meter is enabled we want to have a - few extra updates to make it look smooth. On the - other hand we don't want to waste energy if it - isn't displayed */ - bool pm=false; + /* Skip updates if peak meter disabled. */ + bool peak_meter_enabled = false; FOR_NB_SCREENS(i) + peak_meter_enabled |= skin_get_gwps(skin, i)->data->peak_meter_enabled; + if (!peak_meter_enabled) + return get_action(context, timeout); + + long next_refresh = current_tick; + long timeout_tick = current_tick + timeout; + + while (true) { - if(skin_get_gwps(skin, i)->data->peak_meter_enabled) - pm = true; + int action = get_action(context, TIMEOUT_NOBLOCK); + if (action != ACTION_NONE || !(TIME_BEFORE(current_tick, timeout_tick))) + return action; + + peak_meter_peek(); /* Read peak values */ + sleep(0); /* Sleep until end of current tick. */ + + if (TIME_BEFORE(current_tick, next_refresh)) + continue; + + FOR_NB_SCREENS(i) + if (skin_get_gwps(skin, i)->data->peak_meter_enabled) + skin_update(skin, i, SKIN_REFRESH_PEAK_METER); + + next_refresh += HZ/PEAK_METER_FPS; } - - if (pm) { - long next_refresh = current_tick; - long next_big_refresh = current_tick + timeout; - button = BUTTON_NONE; - while (TIME_BEFORE(current_tick, next_big_refresh)) { - button = get_action(context,TIMEOUT_NOBLOCK); - if (button != ACTION_NONE) { - break; - } - peak_meter_peek(); - sleep(0); /* Sleep until end of current tick. */ - - if (TIME_AFTER(current_tick, next_refresh)) { - FOR_NB_SCREENS(i) - { - if(skin_get_gwps(skin, i)->data->peak_meter_enabled) - skin_update(skin, i, SKIN_REFRESH_PEAK_METER); - next_refresh += HZ / PEAK_METER_FPS; - } - } - } - - } - - /* The peak meter is disabled - -> no additional screen updates needed */ - else - { - button = get_action(context, timeout); - } - return button; } diff --git a/apps/gui/skin_engine/skin_engine.h b/apps/gui/skin_engine/skin_engine.h index 6fe3f7248b..363252794f 100644 --- a/apps/gui/skin_engine/skin_engine.h +++ b/apps/gui/skin_engine/skin_engine.h @@ -67,10 +67,7 @@ void skin_backdrop_unload(int backdrop_id); #define BACKDROP_BUFFERNAME "#backdrop_buffer#" void skin_backdrop_set_buffer(int backdrop_id, struct skin_viewport *svp); -/* do the button loop as often as required for the peak meters to update - * with a good refresh rate. - * gwps is really gwps[NB_SCREENS]! don't wrap this in FOR_NB_SCREENS() - */ +/* Enter button loop updating peak meter at a high refresh rate */ int skin_wait_for_action(enum skinnable_screens skin, int context, int timeout); struct gui_wps *skin_get_gwps(enum skinnable_screens skin, enum screen_type screen); From 30a5f1d858903f053d23ce114338cdbed529cc7e Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Sat, 15 Aug 2026 12:50:48 +0200 Subject: [PATCH 101/408] quickscreen: redraw only the relevant viewports For each setting adjustment, the whole QS was redrawn, including icons that never change at all. Only redraw the viewports that contain a setting that has been adjusted. Change-Id: I74188fbf382a92c6e8bf517d9518c3a96bdd21ef --- apps/gui/quickscreen.c | 140 ++++++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 59 deletions(-) diff --git a/apps/gui/quickscreen.c b/apps/gui/quickscreen.c index af42df6e65..45a0552809 100644 --- a/apps/gui/quickscreen.c +++ b/apps/gui/quickscreen.c @@ -173,11 +173,55 @@ static void quickscreen_fix_viewports(struct quickscreen *qs, enum screen_type s vps[QUICKSCREEN_RIGHT].flags |= VP_FLAG_ALIGN_RIGHT; } +/* Draw QS item into current viewport */ +static void quickscreen_draw_item(struct quickscreen *qs, struct screen *display, + enum quickscreen_item i, bool single_line) +{ + char buf[MAX_PATH]; + int int_value; + unsigned const char *title, *value; + + title = P2STR(ID2P(qs->items[i]->lang_id)); + int_value = option_value_as_int(qs->items[i]); + value = option_get_valuestring(qs->items[i], buf, sizeof buf, int_value); + + if (single_line) + { + char text[MAX_PATH]; + snprintf(text, sizeof text, "%s: %s", title, value); + display->puts_scroll(0, 0, text); + } + else + { + display->puts_scroll(0, 0, title); + display->puts_scroll(0, 1, value); + } +} + +/* Redraw viewports affected by the adjusted setting */ +static void quickscreen_update(struct quickscreen *qs, enum quickscreen_item selected) +{ + FOR_NB_SCREENS(screen) + { + struct screen *display = &screens[screen]; + struct viewport *vps = qs->vps[screen]; + + for (int i = 0; i < QUICKSCREEN_ITEM_COUNT; i++) + if (qs->items[i] == qs->items[selected]) + { + struct viewport *last_vp = display->set_viewport(&vps[i]); + display->clear_viewport(); + quickscreen_draw_item(qs, display, i, viewport_get_nb_lines(&vps[i]) < 2); + display->set_viewport(last_vp); + } + + skin_mark_dirty(screen); + } +} + +/* Redraw whole Quickscreen */ static void quickscreen_draw(struct quickscreen *qs, enum screen_type screen) { - int temp, i; - char buf[MAX_PATH]; - unsigned const char *title, *value; struct screen *display = &screens[screen]; struct viewport *parent = &qs->parent[screen]; struct viewport *vps = qs->vps[screen]; @@ -185,55 +229,33 @@ static void quickscreen_draw(struct quickscreen *qs, enum screen_type screen) struct viewport *last_vp = display->set_viewport(parent); display->clear_viewport(); - for (i = 0; i < QUICKSCREEN_ITEM_COUNT; i++) - { - struct viewport *vp = &vps[i]; - if (!qs->items[i]) - continue; - display->set_viewport(vp); - - title = P2STR(ID2P(qs->items[i]->lang_id)); - temp = option_value_as_int(qs->items[i]); - value = option_get_valuestring(qs->items[i], - buf, sizeof buf, temp); - - if (viewport_get_nb_lines(vp) < 2) + /* items */ + for (int i = 0; i < QUICKSCREEN_ITEM_COUNT; i++) + if (qs->items[i]) { - char text[MAX_PATH]; - snprintf(text, sizeof text, "%s: %s", title, value); - display->puts_scroll(0, 0, text); + display->set_viewport(&vps[i]); + quickscreen_draw_item(qs, display, i, viewport_get_nb_lines(&vps[i]) < 2); } - else - { - display->puts_scroll(0, 0, title); - display->puts_scroll(0, 1, value); - } - } - /* draw the icons */ + + /* icons */ display->set_viewport(vp_icons); - - if (qs->items[QUICKSCREEN_TOP] != NULL) - { + if (qs->items[QUICKSCREEN_TOP]) display->mono_bitmap(bitmap_icons_7x8[Icon_UpArrow], - (vp_icons->width/2) - 4, 0, 7, 8); - } - if (qs->items[QUICKSCREEN_RIGHT] != NULL) - { - display->mono_bitmap(bitmap_icons_7x8[Icon_FastForward], - vp_icons->width - 8, (vp_icons->height/2) - 4, 7, 8); - } - if (qs->items[QUICKSCREEN_LEFT] != NULL) - { - display->mono_bitmap(bitmap_icons_7x8[Icon_FastBackward], - 0, (vp_icons->height/2) - 4, 7, 8); - } - if (qs->items[QUICKSCREEN_BOTTOM] != NULL) - { - display->mono_bitmap(bitmap_icons_7x8[Icon_DownArrow], - (vp_icons->width/2) - 4, vp_icons->height - 8, 7, 8); - } + (vp_icons->width/2) - 4, 0, 7, 8); - skin_mark_dirty(display->screen_type); + if (qs->items[QUICKSCREEN_RIGHT]) + display->mono_bitmap(bitmap_icons_7x8[Icon_FastForward], + vp_icons->width - 8, (vp_icons->height/2) - 4, 7, 8); + + if (qs->items[QUICKSCREEN_LEFT]) + display->mono_bitmap(bitmap_icons_7x8[Icon_FastBackward], + 0, (vp_icons->height/2) - 4, 7, 8); + + if (qs->items[QUICKSCREEN_BOTTOM]) + display->mono_bitmap(bitmap_icons_7x8[Icon_DownArrow], + (vp_icons->width/2) - 4, vp_icons->height - 8, 7, 8); + + skin_mark_dirty(screen); display->set_viewport(last_vp); } @@ -262,39 +284,39 @@ static void talk_qs_option(const struct settings_list *opt, bool enqueue) * - button : the key we are going to analyse * returns : true if the button corresponded to an action, false otherwise */ -static bool quickscreen_do_button(struct quickscreen * qs, int button) +static bool quickscreen_do_button(struct quickscreen * qs, int button, + enum quickscreen_item *item) { - int item; bool previous = false; switch(button) { case ACTION_QS_TOP: - item = QUICKSCREEN_TOP; + *item = QUICKSCREEN_TOP; break; case ACTION_QS_LEFT: - item = QUICKSCREEN_LEFT; + *item = QUICKSCREEN_LEFT; previous = true; break; case ACTION_QS_DOWN: - item = QUICKSCREEN_BOTTOM; + *item = QUICKSCREEN_BOTTOM; previous = true; break; case ACTION_QS_RIGHT: - item = QUICKSCREEN_RIGHT; + *item = QUICKSCREEN_RIGHT; break; default: return false; } - if (qs->items[item] == NULL) + if (qs->items[*item] == NULL) return false; - option_select_next_val(qs->items[item], previous, true); - talk_qs_option(qs->items[item], false); + option_select_next_val(qs->items[*item], previous, true); + talk_qs_option(qs->items[*item], false); return true; } @@ -363,6 +385,7 @@ static void cleanup(void *parameter) static void quickscreen_run(struct quickscreen * qs) { int button; + enum quickscreen_item item; /* To quit we need either : * - a second press on the button that made us enter * - an action taken while pressing the enter button, @@ -407,12 +430,11 @@ static void quickscreen_run(struct quickscreen * qs) qs->result |= QUICKSCREEN_IN_USB; return; } - if (quickscreen_do_button(qs, button)) + if (quickscreen_do_button(qs, button, &item)) { qs->result |= QUICKSCREEN_CHANGED; can_quit = true; - FOR_NB_SCREENS(i) - quickscreen_draw(qs, i); + quickscreen_update(qs, item); } else if (button == qs->button_enter) can_quit = true; From 9cd4352256e3a74a13d090f5fca8302e5b5bb136 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Mon, 17 Aug 2026 05:07:21 +0200 Subject: [PATCH 102/408] quickscreen: fix missing redraw for QS_VOL action If volume was set as one of the Quickscreen items, the value wasn't updated when pressing ACTION_QS_VOLUP or -DOWN. Change-Id: Ie6531535745592d7b53006560f1db8c5b7dbfb97 --- apps/gui/quickscreen.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/gui/quickscreen.c b/apps/gui/quickscreen.c index 45a0552809..083bebe06e 100644 --- a/apps/gui/quickscreen.c +++ b/apps/gui/quickscreen.c @@ -58,6 +58,7 @@ struct quickscreen struct viewport vp_icons[NB_SCREENS]; int button_enter; enum quickscreen_return result; + enum quickscreen_item volume_item; }; static void quickscreen_fix_viewports(struct quickscreen *qs, enum screen_type screen) @@ -438,10 +439,12 @@ static void quickscreen_run(struct quickscreen * qs) } else if (button == qs->button_enter) can_quit = true; - else if (button == ACTION_QS_VOLUP) - adjust_volume(1); - else if (button == ACTION_QS_VOLDOWN) - adjust_volume(-1); + else if (button == ACTION_QS_VOLUP || button == ACTION_QS_VOLDOWN) + { + adjust_volume(button == ACTION_QS_VOLUP ? 1 : -1); + if (qs->volume_item < QUICKSCREEN_ITEM_COUNT) + quickscreen_update(qs, qs->volume_item); + } else if (button == ACTION_STD_CONTEXT) { qs->result |= QUICKSCREEN_GOTO_SHORTCUTS_MENU; @@ -467,6 +470,7 @@ int quick_screen_quick(int button_enter) struct quickscreen qs; qs.button_enter = button_enter; qs.result = QUICKSCREEN_OK; + qs.volume_item = QUICKSCREEN_ITEM_COUNT; for (int i = 0; i < 4; ++i) { @@ -474,6 +478,9 @@ int quick_screen_quick(int button_enter) if (!is_setting_quickscreenable(qs.items[i])) qs.items[i] = NULL; + + if (qs.items[i] && qs.items[i]->lang_id == LANG_VOLUME) + qs.volume_item = i; } quickscreen_run(&qs); From 50b13493d2f83b8644414cefb315658ac653f201 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Sun, 16 Aug 2026 15:36:44 +0200 Subject: [PATCH 103/408] skin_engine: make skin file type available to parser Keep track of the type of skin file being parsed, in case a tag needs this info. E.g. %Lt should be ignored in any skin other than SBS files. Change-Id: Ib7e52c3e6810196cb566adb65619a1b847cf20ef --- apps/gui/skin_engine/skin_engine.c | 4 +- apps/gui/skin_engine/skin_parser.c | 9 ++-- apps/gui/skin_engine/wps_internals.h | 6 ++- tools/checkwps/checkwps.c | 78 ++++++++++++++++++++++------ 4 files changed, 75 insertions(+), 22 deletions(-) diff --git a/apps/gui/skin_engine/skin_engine.c b/apps/gui/skin_engine/skin_engine.c index b7aaf65a6b..e382fb65c0 100644 --- a/apps/gui/skin_engine/skin_engine.c +++ b/apps/gui/skin_engine/skin_engine.c @@ -211,12 +211,12 @@ static void skin_load(enum skinnable_screens skin, enum screen_type screen, skin_helpers[skin]->process(screen, &skins[skin][screen].data, true); if (filename && *filename) - loaded = skin_data_load(screen, &skins[skin][screen].data, + loaded = skin_data_load(skin, screen, &skins[skin][screen].data, filename, true, &skins[skin][screen].stats); if (!loaded && skin_helpers[skin]->default_skin) { - loaded = skin_data_load(screen, &skins[skin][screen].data, + loaded = skin_data_load(skin, screen, &skins[skin][screen].data, skin_helpers[skin]->default_skin(screen), false, &skins[skin][screen].stats); skins[skin][screen].failsafe_loaded = loaded; diff --git a/apps/gui/skin_engine/skin_parser.c b/apps/gui/skin_engine/skin_parser.c index 0bbabb9f3f..14ec327a67 100644 --- a/apps/gui/skin_engine/skin_parser.c +++ b/apps/gui/skin_engine/skin_parser.c @@ -115,8 +115,9 @@ get_param(struct skin_element *element, int param_number) return ¶ms[param_number]; } -/* which screen are we parsing for? */ +/* which screen and skin are we parsing for? */ static enum screen_type curr_screen; +static enum skinnable_screens curr_skin; /* the current viewport */ static struct skin_element *curr_viewport_element; @@ -2593,8 +2594,9 @@ static int skin_element_callback(struct skin_element* element, void* data) /* Set up skin data from a format buffer (isfile = false) or from skin file (isfile = true) */ -bool skin_data_load(enum screen_type screen, struct wps_data *wps_data, - const char *buf, bool isfile, struct skin_stats *stats) +bool skin_data_load(enum skinnable_screens skin, enum screen_type screen, + struct wps_data *wps_data, const char *buf, bool isfile, + struct skin_stats *stats) { char *wps_buffer = NULL; if (!wps_data || !buf) @@ -2624,6 +2626,7 @@ bool skin_data_load(enum screen_type screen, struct wps_data *wps_data, skin_data_reset(wps_data); wps_data->wps_loaded = false; curr_screen = screen; + curr_skin = skin; curr_line = NULL; curr_vp = NULL; curr_viewport_element = NULL; diff --git a/apps/gui/skin_engine/wps_internals.h b/apps/gui/skin_engine/wps_internals.h index 092f0991b0..74c51e9994 100644 --- a/apps/gui/skin_engine/wps_internals.h +++ b/apps/gui/skin_engine/wps_internals.h @@ -25,6 +25,7 @@ #define _WPS_ENGINE_INTERNALS_ #include "tag_table.h" +#include "skin_engine.h" #include "skin_parser.h" #include "gesture.h" #ifndef __PCTOOL__ @@ -46,8 +47,9 @@ bool skin_backdrop_get_debug(int index, char **path, int *ref_count, size_t *siz /* Set up skin data from a format buffer (isfile = false) or from skin file (isfile = true) */ -bool skin_data_load(enum screen_type screen, struct wps_data *wps_data, - const char *buf, bool isfile, struct skin_stats *stats); +bool skin_data_load(enum skinnable_screens skin, enum screen_type screen, + struct wps_data *wps_data, const char *buf, bool isfile, + struct skin_stats *stats); /* Timeout unit expressed in HZ. In WPS, all timeouts are given in seconds (possibly with a decimal fraction) but stored as integer values. diff --git a/tools/checkwps/checkwps.c b/tools/checkwps/checkwps.c index f46e57173d..6534fa9eac 100644 --- a/tools/checkwps/checkwps.c +++ b/tools/checkwps/checkwps.c @@ -278,6 +278,61 @@ struct font* font_get(int font) /* This is no longer defined in ROCKBOX builds so just use a huge value */ #define SKIN_BUFFER_SIZE (200*1024) +int check_filetype(const char *ext, enum skinnable_screens *skin, + enum screen_type *screen) +{ + if (!strcmp(ext, "sbs")) + { + *skin = CUSTOM_STATUSBAR; + *screen = SCREEN_MAIN; + } + else if (!strcmp(ext, "wps")) + { + *skin = WPS; + *screen = SCREEN_MAIN; + } + else if (!strcmp(ext, "fms")) + { +#if CONFIG_TUNER + *skin = FM_SCREEN; + *screen = SCREEN_MAIN; +#else + return 1; +#endif + } + else if (!strcmp(ext, "rsbs")) + { +#ifdef HAVE_REMOTE_LCD + *skin = CUSTOM_STATUSBAR; + *screen = SCREEN_REMOTE; +#else + return 1; /* unsupported, but not an error */ +#endif + } + else if (!strcmp(ext, "rwps")) + { +#ifdef HAVE_REMOTE_LCD + *skin = WPS; + *screen = SCREEN_REMOTE; +#else + return 1; +#endif + } + else if (!strcmp(ext, "rfms")) + { +#if defined(HAVE_REMOTE_LCD) && CONFIG_TUNER + *skin = FM_SCREEN; + *screen = SCREEN_REMOTE; +#else + return 1; +#endif + } + else + return -1; + + return 0; +} + int main(int argc, char **argv) { int ret = 0; @@ -286,6 +341,7 @@ int main(int argc, char **argv) struct wps_data wps={0}; enum screen_type screen = SCREEN_MAIN; + enum skinnable_screens skin; struct screen* wps_screen; /* No arguments -> print the help text @@ -335,28 +391,20 @@ int main(int argc, char **argv) goto done; } ext++; - if (!strcmp(ext, "rwps") || !strcmp(ext, "rsbs") || !strcmp(ext, "rfms")) - { -#ifdef HAVE_REMOTE_LCD - screen = SCREEN_REMOTE; -#else - /* skip rwps etc. if not supported on this target (not an error) */ - continue; -#endif - } - else if (!strcmp(ext, "wps") || !strcmp(ext, "sbs") || !strcmp(ext, "fms")) - { - screen = SCREEN_MAIN; - } - else + + int valid = check_filetype(ext, &skin, &screen); + if (valid < 0) { printf("Invalid extension\n"); ret = 2; goto done; } + else if (valid > 0) + continue; /* skip (unsupported by this target but not an error) */ + wps_screen = &screens[screen]; - res = skin_data_load(screen, &wps, name, true, &stats); + res = skin_data_load(skin, screen, &wps, name, true, &stats); if (!res) { printf("WPS parsing failure\n"); From 789d796120d2b97c588057f5fc839cdb25fbcd00 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Thu, 13 Aug 2026 14:17:00 +0200 Subject: [PATCH 104/408] quickscreen: make %QT hide built-in UI When an SBS uses the new %QT tag (added in 96f42a5) to retrieve the name of the top Quickscreen setting (thus likely wants to draw its own QS UI), we disable the built-in interface until a new SBS file is applied. Fixes lag encountered in Themify 2 due to invisible built-in Quickscreen UI that kept being re-drawn. Change-Id: Id4f295f959c2d16d1650f6fc066c3ceca6685405 --- apps/gui/quickscreen.c | 28 ++++++++++++++---- apps/gui/quickscreen.h | 1 + apps/gui/skin_engine/skin_display.c | 2 +- apps/gui/skin_engine/skin_parser.c | 8 +++++ apps/gui/statusbar-skinned.c | 3 ++ manual/appendix/wps_tags.tex | 45 +++++++++++++++++------------ 6 files changed, 62 insertions(+), 25 deletions(-) diff --git a/apps/gui/quickscreen.c b/apps/gui/quickscreen.c index 083bebe06e..087a879f9c 100644 --- a/apps/gui/quickscreen.c +++ b/apps/gui/quickscreen.c @@ -61,6 +61,16 @@ struct quickscreen enum quickscreen_item volume_item; }; +/* Skin draws custom Quickscreen UI */ +static bool qs_skinned[NB_SCREENS]; + +/* Toggle built-in interface, based on + whether skin draws custom QS UI. */ +void quickscreen_set_skinned(enum screen_type screen, bool skinned) +{ + qs_skinned[screen] = skinned; +} + static void quickscreen_fix_viewports(struct quickscreen *qs, enum screen_type screen) { int char_height, width, pad = 0; @@ -207,6 +217,9 @@ static void quickscreen_update(struct quickscreen *qs, enum quickscreen_item sel struct screen *display = &screens[screen]; struct viewport *vps = qs->vps[screen]; + if (qs_skinned[screen]) + continue; + for (int i = 0; i < QUICKSCREEN_ITEM_COUNT; i++) if (qs->items[i] == qs->items[selected]) { @@ -266,7 +279,8 @@ static void quickscreen_draw_cb(unsigned short id, void *data, void *userdata) (void)data; FOR_NB_SCREENS(i) - quickscreen_draw((struct quickscreen *) userdata, i); + if (!qs_skinned[i]) + quickscreen_draw((struct quickscreen *) userdata, i); } static void talk_qs_option(const struct settings_list *opt, bool enqueue) @@ -372,8 +386,9 @@ static void cleanup(void *parameter) FOR_NB_SCREENS(i) { - for (int j = 0; j < QUICKSCREEN_ITEM_COUNT; j++) - screens[i].scroll_stop_viewport(&qs->vps[i][j]); + if (!qs_skinned[i]) + for (int j = 0; j < QUICKSCREEN_ITEM_COUNT; j++) + screens[i].scroll_stop_viewport(&qs->vps[i][j]); viewportmanager_theme_undo(i, !(qs->result & QUICKSCREEN_GOTO_SHORTCUTS_MENU)); } /* Eliminate flashing of parent during transition to Shortcuts */ @@ -400,8 +415,11 @@ static void quickscreen_run(struct quickscreen * qs) screens[i].set_viewport(NULL); screens[i].scroll_stop(); viewportmanager_theme_enable(i, true, &qs->parent[i]); - quickscreen_fix_viewports(qs, i); - quickscreen_draw(qs, i); + if (!qs_skinned[i]) + { + quickscreen_fix_viewports(qs, i); + quickscreen_draw(qs, i); + } } /* Announce current selection on entering this screen. This is all queued up, but can be interrupted as soon as a setting is diff --git a/apps/gui/quickscreen.h b/apps/gui/quickscreen.h index 1631a62e02..669da7a277 100644 --- a/apps/gui/quickscreen.h +++ b/apps/gui/quickscreen.h @@ -45,6 +45,7 @@ enum quickscreen_return { }; extern int quick_screen_quick(int button_enter); +void quickscreen_set_skinned(enum screen_type screen, bool skinned); bool is_setting_quickscreenable(const struct settings_list *setting); #endif /*_GUI_QUICK_SCREEN_H_*/ diff --git a/apps/gui/skin_engine/skin_display.c b/apps/gui/skin_engine/skin_display.c index 106fd6f85c..a7c023d667 100644 --- a/apps/gui/skin_engine/skin_display.c +++ b/apps/gui/skin_engine/skin_display.c @@ -762,7 +762,7 @@ int skin_wait_for_action(enum skinnable_screens skin, int context, int timeout) /* Skip updates if peak meter disabled. */ bool peak_meter_enabled = false; FOR_NB_SCREENS(i) - peak_meter_enabled |= skin_get_gwps(skin, i)->data->peak_meter_enabled; + peak_meter_enabled |= skin_get_gwps(skin, i)->data->peak_meter_enabled; if (!peak_meter_enabled) return get_action(context, timeout); diff --git a/apps/gui/skin_engine/skin_parser.c b/apps/gui/skin_engine/skin_parser.c index 14ec327a67..8957d9ae49 100644 --- a/apps/gui/skin_engine/skin_parser.c +++ b/apps/gui/skin_engine/skin_parser.c @@ -2450,6 +2450,14 @@ static int skin_element_callback(struct skin_element* element, void* data) case SKIN_TOKEN_DRAW_INBUILTBAR: function = parse_statusbar_tags; break; +#if defined(HAVE_QUICKSCREEN) && !defined(__PCTOOL__) + case SKIN_TOKEN_TOP_QUICKSETTING_NAME: + /* Disable built-in Quickscreen UI if skin draws its own, + as suggested by the use of the %QT tag */ + if (curr_skin == CUSTOM_STATUSBAR) + quickscreen_set_skinned(curr_screen, true); + break; +#endif case SKIN_TOKEN_LIST_TITLE_TEXT: #ifndef __PCTOOL__ sb_skin_has_title(curr_screen); diff --git a/apps/gui/statusbar-skinned.c b/apps/gui/statusbar-skinned.c index 6fcb90ece5..27d3a924f2 100644 --- a/apps/gui/statusbar-skinned.c +++ b/apps/gui/statusbar-skinned.c @@ -101,6 +101,9 @@ void sb_process(enum screen_type screen, struct wps_data *data, bool preprocess) { sbs_loaded[screen] = false; sbs_has_title[screen] = false; +#ifdef HAVE_QUICKSCREEN + quickscreen_set_skinned(screen, false); /* Revert to built-in QS UI */ +#endif viewportmanager_theme_enable(screen, false, NULL); return; } diff --git a/manual/appendix/wps_tags.tex b/manual/appendix/wps_tags.tex index 7c6dbe713a..121bb7a0cf 100644 --- a/manual/appendix/wps_tags.tex +++ b/manual/appendix/wps_tags.tex @@ -319,6 +319,32 @@ Examples: \config{\%?if(\%cs, =, 2)} +\opt{quickscreen}{ +\section{Drawing the Quickscreen using a skin} + +Quickscreen settings can be displayed using the following tags. + +\begin{tagmap} + \config{\%QT} & Top Quickscreen setting (name)\\ + \config{\%Qt} & Top Quickscreen setting (value)\\ + \config{\%QR} & Right Quickscreen setting (name)\\ + \config{\%Qr} & Right Quickscreen setting (value)\\ + \config{\%QB} & Bottom Quickscreen setting (name)\\ + \config{\%Qb} & Bottom Quickscreen setting (value)\\ + \config{\%QL} & Left Quickscreen setting (name)\\ + \config{\%Ql} & Left Quickscreen setting (value)\\ + \end{tagmap} + + Define viewports containing these tags for each Quickscreen position, with your + desired font and formatting. Check if \config{\%cs} is equal to \config{10} + (i.e. Quickscreen is the current screen) before displaying them. + + If \config{\%QT} is present anywhere in the \fname{.sbs}, then the un-skinned + Quickscreen will not be shown. + + \note{If a preset does not match a legal setting, or has no setting applied, all tags will display "ERR".} +} + \section{List Title (\fname{.sbs} only)} \begin{tagmap} \config{\%Lt} & Title text. Should be used in a conditional so that non-list @@ -390,25 +416,6 @@ Examples: \item As a conditional: \config{\%?St(eq enabled)} \end{enumerate} -\opt{quickscreen}{ -\subsection{Displaying Quickscreen settings} - -If your player has a quickscreen, the preset settings can be displayed using the following. - -\begin{tagmap} - \config{\%QT} & Display the name of the top quickscreen setting.\\ - \config{\%Qt} & Display the value of the top quickscreen setting.\\ - \config{\%QR} & Display the name of the right quickscreen setting.\\ - \config{\%Qr} & Display the value of the right quickscreen setting.\\ - \config{\%QB} & Display the name of the bottom quickscreen setting.\\ - \config{\%Qb} & Display the value of the bottom quickscreen setting.\\ - \config{\%QL} & Display the name of the left quickscreen setting.\\ - \config{\%Ql} & Display the value of the left quickscreen setting.\\ - \end{tagmap} - - \note{If a preset does not match a legal setting, or has no setting applied, all tags will display "ERR".} -} - \section{\label{ref:wps_images}Images} \begin{tagmap} \config{\%X(filename.bmp)} From 1f1db58d44fa1076d31f80fa9267d5bf371720da Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Mon, 17 Aug 2026 11:31:46 +0100 Subject: [PATCH 105/408] sdmmc: add optional SCR register to tCardInfo struct The SCR register is needed for detecting if the card supports certain commands like SET_BLOCK_COUNT. Change-Id: I48cbf8fff497b71fb831ba546a462b27bb30851f --- apps/debug_menu.c | 18 ++++++++++++++++++ firmware/export/sdmmc.h | 1 + 2 files changed, 19 insertions(+) diff --git a/apps/debug_menu.c b/apps/debug_menu.c index 031845f32f..51a1fcdc85 100644 --- a/apps/debug_menu.c +++ b/apps/debug_menu.c @@ -1405,6 +1405,24 @@ static int disk_callback(int btn, struct gui_synclist *lists) i_vmin[card_extract_bits(card->csd, 55, 3)], i_vmax[card_extract_bits(card->csd, 52, 3)]); } +#if (CONFIG_STORAGE & STORAGE_SD) + /* + * Most drivers don't read SCR so an all zero value means + * the driver didn't populate it. Valid SCRs will have at + * least the SD_BUS_WIDTHS field containing a nonzero value. + */ + if (card->scr[0] || card->scr[1]) + { + int scr_vers = (card->scr[1] >> 28) & 0xF; + simplelist_addline("SCR version: %d", scr_vers); + + if (scr_vers == 0) + { + bool supports_cmd23 = card->scr[1] & 0x2; + simplelist_addline("CMD23 supported: %s", supports_cmd23 ? "yes" : "no"); + } + } +#endif } else if (card->initialized == 0) { diff --git a/firmware/export/sdmmc.h b/firmware/export/sdmmc.h index 8dbded62b9..c916d31ed9 100644 --- a/firmware/export/sdmmc.h +++ b/firmware/export/sdmmc.h @@ -34,6 +34,7 @@ typedef struct unsigned long ocr; /* OCR register */ unsigned long csd[4]; /* CSD register, 16 bytes */ unsigned long cid[4]; /* CID register, 16 bytes */ + unsigned long scr[2]; /* SCR register, 8 bytes */ unsigned long speed; /* bit/s */ unsigned int nsac; /* clock cycles */ unsigned long taac; /* n * 0.1 ns */ From afe5e7191d30224afd331d111c9a0da94f26843f Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Mon, 17 Aug 2026 11:04:41 +0100 Subject: [PATCH 106/408] sdmmc_host: add helper for allocating disk cache buffer Change-Id: I918a34a92e2e0f53c19ba44fb2b42396719b92e1 --- firmware/drivers/sdmmc_host.c | 43 ++++++++++++++++++++++++----------- firmware/export/sdmmc_host.h | 3 +++ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/firmware/drivers/sdmmc_host.c b/firmware/drivers/sdmmc_host.c index 06d1509573..5fdf3b0898 100644 --- a/firmware/drivers/sdmmc_host.c +++ b/firmware/drivers/sdmmc_host.c @@ -255,6 +255,33 @@ static bool sdmmc_host_medium_present(struct sdmmc_host *host) #endif } +static void *sdmmc_host_get_dc_buffer(struct sdmmc_host *host) +{ + /* + * The disk cache should have a free buffer before the disk + * is mounted. We use the buffer during card initialization + * and release it before doing I/O so the allocation should + * always succeed. + */ + if (!host->dc_buffer) + { + host->dc_buffer = dc_get_buffer(); + if (!host->dc_buffer) + panicf("%s: OOM", __func__); + } + + return host->dc_buffer; +} + +static void sdmmc_host_release_dc_buffer(struct sdmmc_host *host) +{ + if (host->dc_buffer) + { + dc_release_buffer(host->dc_buffer); + host->dc_buffer = NULL; + } +} + /* * Submit one command to the host controller. */ @@ -484,6 +511,7 @@ static int sdmmc_host_cmd_switch_freq(struct sdmmc_host *host, { struct sdmmc_host_command cmd = { .command = SD_SWITCH_FUNC, + .buffer = sdmmc_host_get_dc_buffer(host), .flags = SDMMC_RESP_SHORT | SDMMC_DATA_READ, .nr_blocks = 1, .block_len = 64, @@ -496,19 +524,7 @@ static int sdmmc_host_cmd_switch_freq(struct sdmmc_host *host, else return SDMMC_STATUS_ERROR; - /* - * We'll just assume the disk cache will have a free buffer. - * Since the disk isn't mounted, we should have at least one - * free that would otherwise be used by the FAT filesystem. - */ - cmd.buffer = dc_get_buffer(); - if (!cmd.buffer) - panicf("%s: OOM", __func__); - - int rc = sdmmc_host_submit_cmd(host, &cmd, NULL); - - dc_release_buffer(cmd.buffer); - return rc; + return sdmmc_host_submit_cmd(host, &cmd, NULL); } static int sdmmc_host_cmd_set_block_len(struct sdmmc_host *host, int len) @@ -676,6 +692,7 @@ static int sdmmc_host_transfer(struct sdmmc_host *host, if (!host->initialized) { rc = sdmmc_host_device_init(host); + sdmmc_host_release_dc_buffer(host); if (rc) { host->need_reset = true; diff --git a/firmware/export/sdmmc_host.h b/firmware/export/sdmmc_host.h index 411d73299a..1953b4d1c8 100644 --- a/firmware/export/sdmmc_host.h +++ b/firmware/export/sdmmc_host.h @@ -232,6 +232,9 @@ struct sdmmc_host */ struct mutex lock; + /* Temporary disk cache buffer */ + void *dc_buffer; + /* Bus & device state flags; must only be accessed with lock held */ bool enabled : 1; bool need_reset : 1; From bafcb38ec935328e38a5ee5aa61da596d2559b0f Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Mon, 17 Aug 2026 11:32:10 +0100 Subject: [PATCH 107/408] sdmmc_host: read card SCR register to fill cardinfo struct Change-Id: I2ef8f823d93001f15ea4e794a50f7fc115a55ce6 --- firmware/drivers/sdmmc_host.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/firmware/drivers/sdmmc_host.c b/firmware/drivers/sdmmc_host.c index 5fdf3b0898..1cec921d80 100644 --- a/firmware/drivers/sdmmc_host.c +++ b/firmware/drivers/sdmmc_host.c @@ -466,6 +466,30 @@ static int sdmmc_host_cmd_send_csd(struct sdmmc_host *host) return rc; } +static int sdmmc_host_cmd_send_scr(struct sdmmc_host *host) +{ + struct sdmmc_host_command cmd = { + .command = SD_SEND_SCR, + .buffer = sdmmc_host_get_dc_buffer(host), + .flags = SDMMC_RESP_SHORT | SDMMC_DATA_READ, + .nr_blocks = 1, + .block_len = 8, + }; + + int rc = sdmmc_host_submit_app_cmd(host, &cmd, NULL); + if (rc) + return rc; + + /* + * Card sends the most significant byte first so + * convert the data to native endian. + */ + host->cardinfo.scr[0] = load_be32_aligned(cmd.buffer + 4); + host->cardinfo.scr[1] = load_be32_aligned(cmd.buffer + 0); + + return rc; +} + static int sdmmc_host_cmd_select_card(struct sdmmc_host *host) { struct sdmmc_host_command cmd = { @@ -664,6 +688,13 @@ static int sdmmc_host_device_init(struct sdmmc_host *host) sdmmc_host_set_controller_bus_clock(host, SDMMC_BUS_CLOCK_25MHZ); } + rc = sdmmc_host_cmd_send_scr(host); + if (rc) + { + logf("sdmmc_host_cmd_send_scr: %d", rc); + return rc; + } + host->initialized = true; host->cardinfo.initialized = true; return 0; From cdad8cde6475bdef7103a3a36240e011ff5b8060 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Mon, 17 Aug 2026 11:51:01 +0100 Subject: [PATCH 108/408] sdmmc_host: add support for SET_BLOCK_COUNT (CMD23) According to commit 7327d9fb6c ("Implement set block count (CMD23) for x1000 target") some cards may experience data corruption with certain controllers when CMD12 is used to terminate multiblock writes. Using set block count (CMD23) is reported to fix this issue. Following the approach in that patch, use the SCR register to probe support for CMD23, but disable use at runtime if CMD23 generates an illegal command error. Change-Id: I3ee1e48939b79b848fbda12c6737f2f974f47fa0 --- firmware/drivers/sdmmc_host.c | 41 ++++++++++++++++++++++++++++++++++- firmware/export/sdmmc_host.h | 1 + 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/firmware/drivers/sdmmc_host.c b/firmware/drivers/sdmmc_host.c index 1cec921d80..8db8fcfc92 100644 --- a/firmware/drivers/sdmmc_host.c +++ b/firmware/drivers/sdmmc_host.c @@ -196,6 +196,7 @@ static void sdmmc_host_bus_reset(struct sdmmc_host *host) host->need_reset = false; host->initialized = false; host->is_hcs_card = false; + host->use_cmd23 = false; memset(&host->cardinfo, 0, sizeof(host->cardinfo)); } @@ -487,6 +488,10 @@ static int sdmmc_host_cmd_send_scr(struct sdmmc_host *host) host->cardinfo.scr[0] = load_be32_aligned(cmd.buffer + 4); host->cardinfo.scr[1] = load_be32_aligned(cmd.buffer + 0); + /* Use CMD23 (SET_BLOCK_COUNT) if card reports support */ + if (host->cardinfo.scr[1] & 0x2) + host->use_cmd23 = true; + return rc; } @@ -562,6 +567,28 @@ static int sdmmc_host_cmd_set_block_len(struct sdmmc_host *host, int len) return sdmmc_host_submit_cmd(host, &cmd, NULL); } +static int sdmmc_host_cmd_set_block_count(struct sdmmc_host *host, int count) +{ + struct sdmmc_host_response resp; + struct sdmmc_host_command cmd = { + .command = SD_SET_BLOCK_COUNT, + .argument = count, + .flags = SDMMC_RESP_SHORT, + }; + + int rc = sdmmc_host_submit_cmd(host, &cmd, &resp); + if (rc) + return rc; + + if (resp.data[0] & SD_R1_ILLEGAL_COMMAND) + { + logf("sdmmc: card claimed support for CMD23 but rejected it"); + host->use_cmd23 = false; + } + + return rc; +} + static void sdmmc_host_set_controller_bus_width(struct sdmmc_host *host, uint32_t width) { if (host->ops->set_bus_width) @@ -764,6 +791,18 @@ static int sdmmc_host_transfer(struct sdmmc_host *host, cmd.command = SD_WRITE_MULTIPLE_BLOCK; else cmd.command = SD_READ_MULTIPLE_BLOCK; + + /* + * Note: if the card rejects the command the return code + * will be successful, but use_cmd23 is set to false, and + * CMD12 will be used to terminate the read/write instead. + */ + if (host->use_cmd23) + { + rc = sdmmc_host_cmd_set_block_count(host, xfer_count); + if (rc) + goto out; + } } else { @@ -787,7 +826,7 @@ static int sdmmc_host_transfer(struct sdmmc_host *host, * the end of a transfer, eg. X1000; it might be worth * supporting that via a feature flag. */ - if (xfer_count > 1) + if (xfer_count > 1 && !host->use_cmd23) { memset(&cmd, 0, sizeof(cmd)); cmd.command = SD_STOP_TRANSMISSION; diff --git a/firmware/export/sdmmc_host.h b/firmware/export/sdmmc_host.h index 1953b4d1c8..5258460196 100644 --- a/firmware/export/sdmmc_host.h +++ b/firmware/export/sdmmc_host.h @@ -241,6 +241,7 @@ struct sdmmc_host bool powered : 1; bool initialized : 1; bool is_hcs_card : 1; + bool use_cmd23 : 1; /* Controller implemented by the target */ const struct sdmmc_controller_ops *ops; From 94a0fe608a23906ec89efc23a7945439e373adac Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Tue, 18 Aug 2026 22:26:33 +0100 Subject: [PATCH 109/408] sdmmc_host: trigger LED on disk activity Change-Id: Ic7c43de01f381b7d87ed6b234fa63d5276ae31a5 --- firmware/drivers/sdmmc_host.c | 26 ++++++++++++++++++++++++++ firmware/export/sdmmc_host.h | 1 + 2 files changed, 27 insertions(+) diff --git a/firmware/drivers/sdmmc_host.c b/firmware/drivers/sdmmc_host.c index 8db8fcfc92..337de92561 100644 --- a/firmware/drivers/sdmmc_host.c +++ b/firmware/drivers/sdmmc_host.c @@ -21,6 +21,7 @@ #include "system.h" #include "storage.h" #include "disk_cache.h" +#include "led.h" #include "debug.h" #include "panic.h" #include "logf.h" @@ -48,6 +49,29 @@ DECLARE_FIRST_DRIVE(int sdmmc_mmc_first_drive); static volatile long sdmmc_mmc_last_activity; #endif +static bool sdmmc_host_any_active(void) +{ +#if CONFIG_STORAGE & STORAGE_SD + for (size_t i = 0; i < ARRAYLEN(sdmmc_sd_hosts); ++i) + if (sdmmc_sd_hosts[i]->led_active) + return true; +#endif + +#if CONFIG_STORAGE & STORAGE_MMC + for (size_t i = 0; i < ARRAYLEN(sdmmc_mmc_hosts); ++i) + if (sdmmc_mmc_hosts[i]->led_active) + return true; +#endif + + return false; +} + +static void sdmmc_host_set_led(struct sdmmc_host *host, bool on) +{ + host->led_active = on; + led(sdmmc_host_any_active()); +} + static int sdmmc_host_get_logical_drive(struct sdmmc_host *host) { switch (host->config.type) @@ -734,6 +758,7 @@ static int sdmmc_host_transfer(struct sdmmc_host *host, int rc = -1; mutex_lock(&host->lock); + sdmmc_host_set_led(host, true); if (!host->enabled) goto out; @@ -843,6 +868,7 @@ static int sdmmc_host_transfer(struct sdmmc_host *host, } out: + sdmmc_host_set_led(host, false); mutex_unlock(&host->lock); return rc; } diff --git a/firmware/export/sdmmc_host.h b/firmware/export/sdmmc_host.h index 5258460196..d4a431bbdb 100644 --- a/firmware/export/sdmmc_host.h +++ b/firmware/export/sdmmc_host.h @@ -242,6 +242,7 @@ struct sdmmc_host bool initialized : 1; bool is_hcs_card : 1; bool use_cmd23 : 1; + bool led_active : 1; /* LED active may be read without lock */ /* Controller implemented by the target */ const struct sdmmc_controller_ops *ops; From 5e71adddf990555f3ba16ef3df3cf7101c0402d9 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Wed, 19 Aug 2026 11:05:47 +0100 Subject: [PATCH 110/408] sdmmc_host: add generic SDMMC polling helper Pull this out of the echoplayer target so it can be reused easily by other targets. Change-Id: If14e23a90c133d8435f7944d7b1607630baaf3bb --- firmware/SOURCES | 1 + firmware/drivers/sdmmc_poll.c | 64 +++++++++++++++++++ firmware/export/sdmmc_poll.h | 56 ++++++++++++++++ .../arm/stm32/echoplayer/sdmmc-echoplayer.c | 54 ++-------------- 4 files changed, 128 insertions(+), 47 deletions(-) create mode 100644 firmware/drivers/sdmmc_poll.c create mode 100644 firmware/export/sdmmc_poll.h diff --git a/firmware/SOURCES b/firmware/SOURCES index 488f61ccab..1a064f5c77 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -2106,6 +2106,7 @@ drivers/cw2015.c #endif #ifdef HAVE_SDMMC_HOST drivers/sdmmc_host.c +drivers/sdmmc_poll.c #endif #endif diff --git a/firmware/drivers/sdmmc_poll.c b/firmware/drivers/sdmmc_poll.c new file mode 100644 index 0000000000..4c28e9aea3 --- /dev/null +++ b/firmware/drivers/sdmmc_poll.c @@ -0,0 +1,64 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025-2026 Aidan MacDonald + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include "sdmmc_poll.h" +#include "sdmmc_host.h" + +/* 300ms poll interval */ +#define SDCARD_POLL_TICKS (300 * HZ / 1000) + +static int sdmmc_poll_timeout(struct timeout *tmo) +{ + struct sdmmc_poll *poll = (void *)tmo->data; + + poll->last_state = poll->curr_state; + poll->curr_state = poll->check_inserted(); + + if (!poll->curr_state && poll->is_inserted) + { + poll->is_inserted = false; + sdmmc_host_set_medium_present(poll->host, false); + } + else if (poll->curr_state && !poll->is_inserted && + poll->curr_state == poll->last_state) + { + poll->is_inserted = true; + sdmmc_host_set_medium_present(poll->host, true); + } + + return SDCARD_POLL_TICKS; +} + +void sdmmc_poll_init(struct sdmmc_poll *poll, + struct sdmmc_host *host, + bool (*check_inserted) (void)) +{ + poll->host = host; + poll->check_inserted = check_inserted; + + poll->is_inserted = check_inserted(); + poll->curr_state = poll->is_inserted; + poll->last_state = poll->is_inserted; +} + +void sdmmc_poll_start(struct sdmmc_poll *poll) +{ + timeout_register(&poll->timeout, sdmmc_poll_timeout, + SDCARD_POLL_TICKS, (intptr_t)poll); +} diff --git a/firmware/export/sdmmc_poll.h b/firmware/export/sdmmc_poll.h new file mode 100644 index 0000000000..3472c2b888 --- /dev/null +++ b/firmware/export/sdmmc_poll.h @@ -0,0 +1,56 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025-2026 Aidan MacDonald + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef __SDMMC_POLL_H__ +#define __SDMMC_POLL_H__ + +#include "timeout.h" +#include + +struct sdmmc_host; +struct sdmmc_poll +{ + struct timeout timeout; + struct sdmmc_host *host; + + bool (*check_inserted)(void); + + bool is_inserted; + bool last_state; + bool curr_state; +}; + +/* + * Simple helper for polling for SD card insertion state. + * This calls sdmmc_host_set_medium_present() when a change + * in insertion state is detected via the check_inserted() + * callback. + * + * Call sdmmc_poll_start() to start the poll timer. + * + * Changes in the insertion state are debounced before they + * are reported to the sdmmc_host. + */ + +void sdmmc_poll_init(struct sdmmc_poll *poll, + struct sdmmc_host *host, + bool (*check_inserted) (void)); +void sdmmc_poll_start(struct sdmmc_poll *poll); + +#endif /* __SDMMC_POLL_H__ */ diff --git a/firmware/target/arm/stm32/echoplayer/sdmmc-echoplayer.c b/firmware/target/arm/stm32/echoplayer/sdmmc-echoplayer.c index 61ab336841..ac4f2023e0 100644 --- a/firmware/target/arm/stm32/echoplayer/sdmmc-echoplayer.c +++ b/firmware/target/arm/stm32/echoplayer/sdmmc-echoplayer.c @@ -18,16 +18,15 @@ * ****************************************************************************/ #include "sdmmc_host.h" +#include "sdmmc_poll.h" #include "clock-echoplayer.h" #include "sdmmc-stm32h7.h" #include "gpio-stm32h7.h" #include "nvic-arm.h" #include "regs/stm32h743/sdmmc.h" -/* 300ms poll interval */ -#define SDCARD_POLL_TICKS (300 * HZ / 1000) - static struct sdmmc_host sdmmc1; +static struct sdmmc_poll sdmmc1_poll; static struct stm32h7_sdmmc_controller sdmmc1_ctl; static const struct sdmmc_controller_ops sdmmc_ops = { @@ -51,48 +50,11 @@ static const struct sdmmc_host_config sdmmc_config INITDATA_ATTR = { .is_removable = true, }; -/* - * simple SD insertion poller - */ -struct sdmmc_poll -{ - struct sdmmc_host *host; - bool is_inserted; - - bool last_state; - bool curr_state; -}; - static bool is_sdcard_inserted(void) { return gpio_get_level(GPIO_SDMMC_DETECT) == 0; } -static int poll_sdcard_inserted(struct timeout *tmo) -{ - struct sdmmc_poll *poll = (void *)tmo->data; - - poll->last_state = poll->curr_state; - poll->curr_state = is_sdcard_inserted(); - - if (!poll->curr_state && poll->is_inserted) - { - poll->is_inserted = false; - sdmmc_host_set_medium_present(poll->host, false); - } - else if (poll->curr_state && !poll->is_inserted && - poll->curr_state == poll->last_state) - { - poll->is_inserted = true; - sdmmc_host_set_medium_present(poll->host, true); - } - - return SDCARD_POLL_TICKS; -} - -static struct timeout sdcard_poll_timeout; -static struct sdmmc_poll sdcard_poll; - void sdmmc_host_target_init(void) { /* Initialize controller */ @@ -100,15 +62,13 @@ void sdmmc_host_target_init(void) stm32h7_reset_sdmmc1, NULL); nvic_enable_irq(NVIC_IRQN_SDMMC1); - /* Initialize card detect polling */ - sdcard_poll.host = &sdmmc1; - sdcard_poll.is_inserted = is_sdcard_inserted(); - timeout_register(&sdcard_poll_timeout, poll_sdcard_inserted, - SDCARD_POLL_TICKS, (intptr_t)&sdcard_poll); - /* Initialize SD/MMC host driver */ sdmmc_host_init(&sdmmc1, &sdmmc_config, &sdmmc_ops, &sdmmc1_ctl); - sdmmc_host_init_medium_present(&sdmmc1, sdcard_poll.is_inserted); + sdmmc_host_init_medium_present(&sdmmc1, is_sdcard_inserted()); + + /* Start insertion poller */ + sdmmc_poll_init(&sdmmc1_poll, &sdmmc1, is_sdcard_inserted); + sdmmc_poll_start(&sdmmc1_poll); } void sdmmc1_irq_handler(void) From 69a36026f89247a076ee8ca5232d068f241e6879 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Wed, 19 Aug 2026 11:07:05 +0100 Subject: [PATCH 111/408] sdmmc_poll: add edge triggered polling support Change-Id: I958a09efc055d474da5e03a06d80719dbb2f5b14 --- firmware/drivers/sdmmc_poll.c | 13 ++++++++++++- firmware/export/sdmmc_poll.h | 8 +++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/firmware/drivers/sdmmc_poll.c b/firmware/drivers/sdmmc_poll.c index 4c28e9aea3..1e1d9d5baf 100644 --- a/firmware/drivers/sdmmc_poll.c +++ b/firmware/drivers/sdmmc_poll.c @@ -42,7 +42,10 @@ static int sdmmc_poll_timeout(struct timeout *tmo) sdmmc_host_set_medium_present(poll->host, true); } - return SDCARD_POLL_TICKS; + if (poll->is_polling || (poll->curr_state != poll->last_state)) + return SDCARD_POLL_TICKS; + + return 0; } void sdmmc_poll_init(struct sdmmc_poll *poll, @@ -59,6 +62,14 @@ void sdmmc_poll_init(struct sdmmc_poll *poll, void sdmmc_poll_start(struct sdmmc_poll *poll) { + poll->is_polling = true; timeout_register(&poll->timeout, sdmmc_poll_timeout, SDCARD_POLL_TICKS, (intptr_t)poll); } + +void sdmmc_poll_event(struct sdmmc_poll *poll) +{ + timeout_register(&poll->timeout, sdmmc_poll_timeout, + SDCARD_POLL_TICKS, (intptr_t)poll); + sdmmc_poll_timeout(&poll->timeout); +} diff --git a/firmware/export/sdmmc_poll.h b/firmware/export/sdmmc_poll.h index 3472c2b888..3323644e2f 100644 --- a/firmware/export/sdmmc_poll.h +++ b/firmware/export/sdmmc_poll.h @@ -32,6 +32,7 @@ struct sdmmc_poll bool (*check_inserted)(void); bool is_inserted; + bool is_polling; bool last_state; bool curr_state; }; @@ -42,7 +43,11 @@ struct sdmmc_poll * in insertion state is detected via the check_inserted() * callback. * - * Call sdmmc_poll_start() to start the poll timer. + * Call sdmmc_poll_start() to start the poll timer if you + * use level triggered polling. + * + * To use edge triggered events call sdmmc_poll_event() if + * the return value of check_inserted() may have changed. * * Changes in the insertion state are debounced before they * are reported to the sdmmc_host. @@ -52,5 +57,6 @@ void sdmmc_poll_init(struct sdmmc_poll *poll, struct sdmmc_host *host, bool (*check_inserted) (void)); void sdmmc_poll_start(struct sdmmc_poll *poll); +void sdmmc_poll_event(struct sdmmc_poll *poll); #endif /* __SDMMC_POLL_H__ */ From aeaee5da45c9e99d6dc4bdb2631418ce1843e5ad Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sat, 15 Aug 2026 22:57:35 -0400 Subject: [PATCH 112/408] voice.pl: Extract PERFORMER and TITLE from cuesheets Change-Id: Ib83b0c7bc74777ce6d78a6c9507c3d0732552334 --- tools/voice.pl | 82 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/tools/voice.pl b/tools/voice.pl index 6eb5d6f4e3..53c153f449 100755 --- a/tools/voice.pl +++ b/tools/voice.pl @@ -754,6 +754,37 @@ sub panic_cleanup { die "moo"; } +sub gensingleclip { + our $verbose; + my ($enc, $voice, $tts_object, $language, $encoder, $encoder_opts, $tts_engine_opts) = @_; + + my $format = $tts_object->{'format'}; + my $wav = sprintf("%s.talk.wav", $enc); + + #Apply corrections + $voice = correct_string($voice, $language, $tts_object); + + printf("Talkclip %s: %s\n", $enc, $voice) if $verbose; + # Don't generate encoded file if it already exists + return if (-f $enc && !$force); + + voicestring($voice, $wav, $tts_engine_opts, $tts_object); + wavtrim($wav, $trim_thresh, $tts_object); + + if ($format eq "mp3") { + system("ffmpeg -loglevel 0 -i $wav $voice$wav"); + rename("$voice$wav","$wav"); + $format = "wav"; + } + if ($format eq "wav") { + encodewav($wav, $enc, $encoder, $encoder_opts, $tts_object); + } else { + copy($wav, $enc); + } + synchronize($tts_object); + unlink($wav); +} + # Generate .talk clips sub gentalkclips { our $verbose; @@ -762,8 +793,7 @@ sub gentalkclips { while (my $file = $d->read) { $file = Encode::decode( locale_fs => $file); - my ($voice, $wav, $enc); - my $format = $tts_object->{'format'}; + my ($voice, $enc); # Ignore dot-dirs and talk files if ($file eq '.' || $file eq '..' || $file =~ /\.talk$/) { @@ -783,40 +813,38 @@ sub gentalkclips { # Convert to a complete path my $path = sprintf("%s/%s", $dir, $file); - $wav = sprintf("%s.talk.wav", $path); - if ( -d $path) { # Element is a dir $enc = sprintf("%s/_dirname.talk", $path); if (! -e "$path/talkclips.ignore") { # Skip directories containing "talkclips.ignore" gentalkclips($path, $tts_object, $language, $encoder, $encoder_opts, $tts_engine_opts, $i); } + gensingleclip($enc, $voice, $tts_object, $language, $encoder, $encoder_opts, $tts_engine_opts); + } elsif ($path =~ /\.cue$/i) { + # Cuesheet. Generate a clip for the cuesheet itself... + $enc = sprintf("%s.talk", $path); + $voice =~ s/\.[^\.]*$//; # Trim extension + gensingleclip($enc, $voice, $tts_object, $language, $encoder, $encoder_opts, $tts_engine_opts); + + # Parse the cuesheet + open(CUE, "<$path"); + + while() { + chomp; + if (/PERFORMER/ || /TITLE/) { + /^\s*(TITLE|PERFORMER)\s+["'](.*)["']/; + $voice = $2; + $enc = dirname($path) . "/$voice.talk"; + #print ("\V: ``$voice''\n -> $enc\n"); + gensingleclip($enc, $voice, $tts_object, $language, $encoder, $encoder_opts, $tts_engine_opts); + } + } + + close(CUE); } else { # Element is a file $enc = sprintf("%s.talk", $path); $voice =~ s/\.[^\.]*$//; # Trim extension + gensingleclip($enc, $voice, $tts_object, $language, $encoder, $encoder_opts, $tts_engine_opts); } - - # Apply corrections - $voice = correct_string($voice, $language, $tts_object); - - printf("Talkclip %s: %s\n", $enc, $voice) if $verbose; - # Don't generate encoded file if it already exists - next if (-f $enc && !$force); - - voicestring($voice, $wav, $tts_engine_opts, $tts_object); - wavtrim($wav, $trim_thresh, $tts_object); - - if ($format eq "mp3") { - system("ffmpeg -loglevel 0 -i $wav $voice$wav"); - rename("$voice$wav","$wav"); - $format = "wav"; - } - if ($format eq "wav") { - encodewav($wav, $enc, $encoder, $encoder_opts, $tts_object); - } else { - copy($wav, $enc); - } - synchronize($tts_object); - unlink($wav); } } From c04c2c3e0b690ba7fa11227978c43d19c80eb393 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sat, 15 Aug 2026 21:08:14 -0400 Subject: [PATCH 113/408] FS13892: Voice feedback in Cuesheet browser Voices the index, performer, and title of each cuesheet entry. Uses talkclips if any are available. Change-Id: If54931cb5bbeb698f23146479192c1d6fe387657 --- apps/cuesheet.c | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/cuesheet.c b/apps/cuesheet.c index e30e5ca404..9b27c410dd 100644 --- a/apps/cuesheet.c +++ b/apps/cuesheet.c @@ -447,6 +447,30 @@ static const char* list_get_name_cb(int selected_item, return buffer; } +static char global_temp_buffer[MAX_PATH+1]; + +static int cuesheet_list_voice_cb(int list_index, void* data) +{ + struct cuesheet *cue = (struct cuesheet *) data; + int index = list_index / 2; + + talk_id(LANG_PLAYTIME_TRACK, true); + talk_number(index + 1, true); + + /* Look up and voice talk clips */ + const char *nameptr = NULL; + size_t dirlen = path_dirname(cue->file, &nameptr); + size_t tmp; + tmp = snprintf(global_temp_buffer, dirlen + 1, "%s", cue->file); + global_temp_buffer[tmp] = 0; + talk_file_or_spell(global_temp_buffer, cue->tracks[index].performer, + TALK_IDARRAY(LANG_ID3_ARTIST), true); + talk_file_or_spell(global_temp_buffer, cue->tracks[index].title, + TALK_IDARRAY(LANG_ID3_TITLE), true); + + return 0; +} + void browse_cuesheet(struct cuesheet *cue) { struct gui_synclist lists; @@ -463,11 +487,12 @@ void browse_cuesheet(struct cuesheet *cue) if ((unsigned) len > sizeof(title)) title[sizeof(title) - 2] = '~'; /* give indication of truncation */ - gui_synclist_init(&lists, list_get_name_cb, cue, false, 2, NULL); gui_synclist_set_nb_items(&lists, 2*cue->track_count); gui_synclist_set_title(&lists, title, 0); + if(global_settings.talk_menu) + gui_synclist_set_voice_callback(&lists, cuesheet_list_voice_cb); if (id3) { @@ -475,6 +500,8 @@ void browse_cuesheet(struct cuesheet *cue) 2*cue_find_current_track(cue, id3->elapsed)); } + gui_synclist_speak_item(&lists); + while (!done) { gui_synclist_draw(&lists); @@ -503,10 +530,9 @@ void browse_cuesheet(struct cuesheet *cue) if (!startit || !*cue->file) break; - char file[MAX_PATH]; - strmemccpy(file, cue->file, MAX_PATH); - char *fname = strrsplt(file, '/'); - char *dirname = fname <= file + 1 ? "/" : file; + strmemccpy(global_temp_buffer, cue->file, MAX_PATH); + char *fname = strrsplt(global_temp_buffer, '/'); + char *dirname = fname <= global_temp_buffer + 1 ? "/" : global_temp_buffer; bookmark_play(dirname, 0, elapsed, 0, current_tick, fname); break; } /* ACTION_STD_OK */ From 7fef95dc0866e55f1435b3736bcd3c11e4469071 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Sat, 22 Aug 2026 23:04:02 -0400 Subject: [PATCH 114/408] warble: fix build on targets with HAVE_RECORDING Change-Id: Ic04392da41177f51dd5f392b288e078a76298839 --- lib/rbcodec/codecs/SOURCES | 5 ++- lib/rbcodec/test/warble.c | 88 ++++++++++++++++++++++++++++++++------ 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/lib/rbcodec/codecs/SOURCES b/lib/rbcodec/codecs/SOURCES index 44b79087fe..cc097546d4 100644 --- a/lib/rbcodec/codecs/SOURCES +++ b/lib/rbcodec/codecs/SOURCES @@ -47,7 +47,8 @@ kss.c #endif aac_bsf.c -#ifdef HAVE_RECORDING +/* warble is decoder-only */ +#if defined(HAVE_RECORDING) && !defined(WARBLE) /* encoders */ @@ -56,4 +57,4 @@ mp3_enc.c wav_enc.c wavpack_enc.c -#endif /* HAVE_RECORDING */ +#endif /* HAVE_RECORDING && !WARBLE */ diff --git a/lib/rbcodec/test/warble.c b/lib/rbcodec/test/warble.c index fa243a3913..75591b99dc 100644 --- a/lib/rbcodec/test/warble.c +++ b/lib/rbcodec/test/warble.c @@ -647,6 +647,73 @@ static void ci_logf(const char *fmt, ...) static void stub_void_void(void) { } +#ifdef HAVE_RECORDING +/* warble only ever loads decoders, so the encoder half of the codec API is + never entered. Fail loudly instead of leaving these NULL, which would turn + a mistake into a segfault. */ +static void stub_enc_unsupported(const char *func) +{ + fprintf(stderr, "error: warble does not support encoding (%s)\n", func); + abort(); +} + +static int ci_enc_pcmbuf_read(void *buf, int count) +{ + (void)buf; (void)count; + stub_enc_unsupported(__func__); + return 0; +} + +static int ci_enc_pcmbuf_advance(int count) +{ + (void)count; + stub_enc_unsupported(__func__); + return 0; +} + +static struct enc_chunk_data *ci_enc_encbuf_get_buffer(size_t need) +{ + (void)need; + stub_enc_unsupported(__func__); + return NULL; +} + +static void ci_enc_encbuf_finish_buffer(void) +{ + stub_enc_unsupported(__func__); +} + +static ssize_t ci_enc_stream_read(void *buf, size_t count) +{ + (void)buf; (void)count; + stub_enc_unsupported(__func__); + return -1; +} + +static off_t ci_enc_stream_lseek(off_t offset, int whence) +{ + (void)offset; (void)whence; + stub_enc_unsupported(__func__); + return -1; +} + +static ssize_t ci_enc_stream_write(const void *buf, size_t count) +{ + (void)buf; (void)count; + stub_enc_unsupported(__func__); + return -1; +} + +static int ci_round_value_to_list32(unsigned long value, + const unsigned long list[], + int count, bool signd) +{ + (void)value; (void)list; (void)count; (void)signd; + stub_enc_unsupported(__func__); + return 0; +} +#endif /* HAVE_RECORDING */ + static struct codec_api ci = { 0, /* filesize */ @@ -703,21 +770,14 @@ static struct codec_api ci = { qsort, #ifdef HAVE_RECORDING - ci_enc_get_inputs, - ci_enc_set_parameters, - ci_enc_get_chunk, - ci_enc_finish_chunk, - ci_enc_get_pcm_data, - ci_enc_unget_pcm_data, - - /* file */ - open, - close, - read, - lseek, - write, + ci_enc_pcmbuf_read, + ci_enc_pcmbuf_advance, + ci_enc_encbuf_get_buffer, + ci_enc_encbuf_finish_buffer, + ci_enc_stream_read, + ci_enc_stream_lseek, + ci_enc_stream_write, ci_round_value_to_list32, - #endif /* HAVE_RECORDING */ }; From 5d016a2c044c7209a51155b573f10fdc0e165113 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Sat, 22 Aug 2026 23:16:58 -0400 Subject: [PATCH 115/408] Fix building Warble when configured as the iPod Video Change-Id: If229f29d31a0a0d012d5ba210c16d1f1ea74a217 --- firmware/core_alloc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/core_alloc.c b/firmware/core_alloc.c index 95642e806b..1e2477c1af 100644 --- a/firmware/core_alloc.c +++ b/firmware/core_alloc.c @@ -61,7 +61,7 @@ void core_allocator_init(void) { unsigned char *start = ALIGN_UP(audiobuffer, sizeof(intptr_t)); -#if defined(IPOD_VIDEO) && !defined(BOOTLOADER) && !defined(SIMULATOR) +#if !defined(__PCTOOL__) && defined(IPOD_VIDEO) && !defined(BOOTLOADER) && !defined(SIMULATOR) audiobufend=(unsigned char *)audiobufend_lds; if(MEMORYSIZE==64 && probed_ramsize!=64) { From ec59f570ddc04349b7df500d364c0b8a36739c27 Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Sat, 25 Jul 2026 23:47:58 +0100 Subject: [PATCH 116/408] keyboard: point-mode virtual keyboard for touchscreens On large touchscreens the software keyboard can now run in "point mode". Keys are laid out finger-sized and tapped directly instead of being navigated with a cursor. Targets opt in with HAVE_KBD_POINT_MODE. Key size is derived from LCD_DPI so a key is about 5mm across whatever the panel density, falling back to a fixed size where DPI is unknown. A phone-style default layout in the UI font, across three flip pages of eight lines. Page two is page one shifted (i.e caps). Page three completes Latin-1 and adds additional accented letters common to the other European locales. Space is drawn as an icon, since the UI font has no glyph for it. Change-Id: Ibe2e305b3c22b1f9152358cf3864b9445b67b463 Co-Author: Claude Opus 4.8 --- apps/features.txt | 4 + apps/recorder/keyboard.c | 173 ++++++++++++++++-- firmware/export/config/hibyr1.h | 2 + firmware/export/config/hibyr3proii.h | 2 + .../browsing_and_playing.tex | 10 + .../images/ss-virtual-keyboard-480x720x16.png | Bin 0 -> 4028 bytes .../images/ss-virtual-keyboard-480x800x16.png | Bin 0 -> 4042 bytes 7 files changed, 177 insertions(+), 14 deletions(-) create mode 100644 manual/rockbox_interface/images/ss-virtual-keyboard-480x720x16.png create mode 100644 manual/rockbox_interface/images/ss-virtual-keyboard-480x800x16.png diff --git a/apps/features.txt b/apps/features.txt index 5453ec86bc..0c4719ca11 100644 --- a/apps/features.txt +++ b/apps/features.txt @@ -82,6 +82,10 @@ lcd_sleep morse_input #endif +#if defined(HAVE_KBD_POINT_MODE) +kbd_point_mode +#endif + #if defined(HAVE_PITCHCONTROL) pitchscreen #endif diff --git a/apps/recorder/keyboard.c b/apps/recorder/keyboard.c index ee47470bb1..cb00fef74f 100644 --- a/apps/recorder/keyboard.c +++ b/apps/recorder/keyboard.c @@ -47,8 +47,23 @@ #define DEFAULT_MARGIN 6 #define KBD_BUF_SIZE 500 +/* Sentinel picker cell, drawn as an icon (the UI font has no glyph for it): + * KBD_SPACE inserts a space. */ +#define KBD_SPACE 0x2423 + #ifdef HAVE_TOUCHSCREEN +/* Increase grid size so it responds better to taps */ +#ifdef HAVE_KBD_POINT_MODE +#ifndef MIN_GRID_SIZE +#if defined(LCD_DPI) && (LCD_DPI > 0) +#define MIN_GRID_SIZE ((LCD_DPI * 50 + 127) / 254) +#else +#define MIN_GRID_SIZE 44 +#endif +#endif +#else #define MIN_GRID_SIZE 16 +#endif #define GRID_SIZE(s, x) \ ((s) == SCREEN_MAIN && MIN_GRID_SIZE > (x) ? MIN_GRID_SIZE: (x)) #endif @@ -389,6 +404,29 @@ static ucschar_t get_kbd_ch(struct keyboard_parameters *pm, int x, int y) k = k * pm->max_chars + x; return (*pbuf != 0xFEFF && k < *pbuf)? pbuf[k+1]: ' '; } + +#ifdef HAVE_TOUCHSCREEN +/* Number of real characters in the grid row drawn at picker row y. */ +static int get_kbd_row_len(struct keyboard_parameters *pm, int y) +{ + unsigned int n, i, k = pm->page*pm->lines + y; + ucschar_t *pbuf; + for (pbuf = pm->kbd_buf_ptr; (i = *pbuf) != 0xFEFF; pbuf += i + 1) + { + n = i ? (i + pm->max_chars - 1) / pm->max_chars : 1; + if (k < n) + { + int len = (int)i - (int)(k * pm->max_chars); + if (len > (int)pm->max_chars) + len = pm->max_chars; + return len < 0 ? 0 : len; + } + k -= n; + } + return 0; +} +#endif /* HAVE_TOUCHSCREEN */ + static void kbd_calc_pm_params(struct keyboard_parameters *pm, struct screen *sc, struct edit_state *state); static void kbd_calc_vp_params(struct keyboard_parameters *pm, @@ -430,10 +468,14 @@ int kbd_input(char* text, int buflen, ucschar_t *kbd) } #ifdef HAVE_TOUCHSCREEN + enum touchscreen_mode old_mode = touchscreen_get_mode(); +#ifdef HAVE_KBD_POINT_MODE + touchscreen_set_mode(TOUCHSCREEN_POINT); +#else /* keyboard is unusuable in pointing mode so force 3x3 for now. * TODO - fix properly by using a bigger font and changing the layout */ - enum touchscreen_mode old_mode = touchscreen_get_mode(); touchscreen_set_mode(TOUCHSCREEN_BUTTON); +#endif #endif /* initialize state */ state.text = text; @@ -458,6 +500,48 @@ int kbd_input(char* text, int buflen, ucschar_t *kbd) const unsigned char *p; int len = 0; +#ifdef HAVE_KBD_POINT_MODE + if (touchscreen_get_mode() == TOUCHSCREEN_POINT) + { +#if (LCD_WIDTH >= 480) && (LCD_HEIGHT >= 640) + /* Three pages of eight lines. Page two is page one shifted, cell + * for cell; page three completes Latin-1 and adds the accented + * letters common to the other European locales. The space bar + * occupies the same cells on every page. Ten columns need the + * width, and all eight lines have to be visible at once. */ + p = "1234567890\n" + "qwertyuiop\n" + "asdfghjkl;\n" + "zxcvbnm,./\n" + "àáâäåçèéêë\n" + "ìíîïñòóôöù\n" + "úûüÿ'æøðþý\n" + "`-=␣␣␣␣[]\\\n" + "!@#$%^&*()\n" + "QWERTYUIOP\n" + "ASDFGHJKL:\n" + "ZXCVBNM<>?\n" + "ÀÁÂÄÅÇÈÉÊË\n" + "ÌÍÎÏÑÒÓÔÖÙ\n" + "ÚÛÜŸ\"ÆØÐÞÝ\n" + "~_+␣␣␣␣{}|\n" + "¡¢£¤¥¦§¨©ª\n" + "«¬®¯°±²³´µ\n" + "¶·¸¹º»¼½¾¿\n" + "×÷ßÃãÕõ€Œœ\n" + "ĐđČčĎďĚěŇň\n" + "ŘřŠšŤťŮůŽž\n" + "ĄąĆćĘꣳŃń\n" + "ŚśŹ␣␣␣␣źŻż"; + pm->default_lines = 8; + pm->max_line_len = 10; +#else +# error "HAVE_KBD_POINT_MODE: no point-mode layout for this screen size" +#endif + } + else +#endif + { #if LCD_WIDTH >= 160 && LCD_HEIGHT >= 96 struct screen *sc = &screens[l]; @@ -528,6 +612,7 @@ int kbd_input(char* text, int buflen, ucschar_t *kbd) pm->max_line_len = 18; } #endif + } pbuf = pm->kbd_buf; while (*p) { @@ -849,6 +934,11 @@ static void kbd_calc_pm_params(struct keyboard_parameters *pm, (touchscreen_get_mode() == TOUCHSCREEN_POINT)); #endif +#ifdef HAVE_TOUCHSCREEN + if (touchscreen_get_mode() == TOUCHSCREEN_POINT) + pm->curfont = sc->getuifont(); + else +#endif pm->curfont = pm->default_lines ? FONT_SYSFIXED : sc->getuifont(); font = font_get(pm->curfont); pm->font_h = font->height; @@ -907,6 +997,11 @@ static void kbd_calc_vp_params(struct keyboard_parameters *pm, icon_w = get_icon_width(sc->screen_type); sc_w = vp->width; /**sc->getwidth();**/ +#ifdef HAVE_TOUCHSCREEN + if (touchscreen_get_mode() == TOUCHSCREEN_POINT) + pm->font_w = sc_w / pm->max_line_len; /* fill width, allow >10 cols */ + else +#endif if (pm->font_w < sc_w / pm->max_line_len) pm->font_w = sc_w / pm->max_line_len; pm->max_chars = sc_w / pm->font_w; @@ -1075,22 +1170,47 @@ static void kbd_draw_picker(struct keyboard_parameters *pm, { for (i = 0; i < pm->max_chars; i++) { + int cx = i*pm->font_w, cy = j*pm->font_h; ch = get_kbd_ch(pm, i, j); + if (ch == KBD_SPACE) + { + /* space bar: a bar with short upturned ends. Cells join; + * the run's first/last cell adds the vertical end tick. */ + int th = MAX(2, pm->font_h/14); + int by = cy + (pm->font_h*2)/3; + int tk = pm->font_h/6; + int rx = cx + pm->font_w - th; + sc->fillrect(cx, by, pm->font_w, th); + if (i == 0 || get_kbd_ch(pm, i-1, j) != KBD_SPACE) + sc->fillrect(cx, by - tk, th, tk + th); + if (get_kbd_ch(pm, i+1, j) != KBD_SPACE) + sc->fillrect(rx, by - tk, th, tk + th); + continue; + } utf8 = utf8encode(ch, outline); *utf8 = 0; sc->getstringsize(outline, &w, &h); - sc->putsxy(i*pm->font_w + (pm->font_w-w) / 2, - j*pm->font_h + (pm->font_h-h) / 2, outline); + sc->putsxy(cx + (pm->font_w-w) / 2, + cy + (pm->font_h-h) / 2, outline); } } if (!pm->line_edit) { - /* highlight the key that has focus */ + /* highlight the focused key; the space bar spans several cells and + * highlights as one */ + int hx = pm->x, hw = 1; + if (get_kbd_ch(pm, hx, pm->y) == KBD_SPACE) + { + while (hx > 0 && get_kbd_ch(pm, hx - 1, pm->y) == KBD_SPACE) + hx--; + while (get_kbd_ch(pm, hx + hw, pm->y) == KBD_SPACE) + hw++; + } sc->set_drawmode(DRMODE_COMPLEMENT); - sc->fillrect(pm->font_w*pm->x, pm->font_h*pm->y, - pm->font_w, pm->font_h); + sc->fillrect(pm->font_w*hx, pm->font_h*pm->y, + pm->font_w*hw, pm->font_h); sc->set_drawmode(DRMODE_SOLID); } } @@ -1337,8 +1457,8 @@ static void kbd_draw_buttons(struct keyboard_parameters *pm, struct screen *sc) /* draw buttons */ if (pm->pages > 1) { - /* button to flip page. */ - vp.y = pm->lines*pm->font_h; + /* button to flip page */ + vp.y = pm->kbd_viewports[eKBD_VP_PICKER].y + pm->lines*pm->font_h; sc->hline(0, sc_w - 1, 0); sc->putsxy(0, text_y, ">"); } @@ -1378,22 +1498,32 @@ static int keyboard_touchscreen(struct keyboard_parameters *pm, #endif if (x < 0 || y < 0) return ACTION_NONE; - if (y < pm->lines*pm->font_h) + /* The picker grid is drawn in its own viewport, below the edit line, so + * translate the absolute touch into that viewport (see kbd_draw_picker). */ + struct viewport *picker_vp = &pm->kbd_viewports[eKBD_VP_PICKER]; + int picker_x = x - picker_vp->x; + int picker_y = y - picker_vp->y; + if (picker_y >= 0 && picker_y < pm->lines*pm->font_h) { - if (x/pm->font_w < pm->max_chars) + int row = picker_y/pm->font_h; + /* ignore taps past the end of a short row */ + if (picker_x >= 0 && picker_x/pm->font_w < get_kbd_row_len(pm, row)) { /* picker area */ state->changed = CHANGED_PICKER; - pm->x = x/pm->font_w; - pm->y = y/pm->font_h; + pm->x = picker_x/pm->font_w; + pm->y = row; pm->line_edit = false; if (button == BUTTON_REL) return ACTION_KBD_SELECT; } } - else if (y < pm->main_y - pm->keyboard_margin) + else if (pm->pages > 1 && picker_y >= pm->lines*pm->font_h && + picker_y < pm->lines*pm->font_h + + GRID_SIZE(sc->screen_type, sc->getcharheight())) { - /* button to flip page */ + /* flip-page button, drawn just below the grid; test the same band in + * picker-relative coords so ">" is tappable. */ if (button == BUTTON_REL) return ACTION_KBD_PAGE_FLIP; } @@ -1432,6 +1562,9 @@ static void kbd_insert_selected(struct keyboard_parameters *pm, /* find input char */ ucschar_t ch = get_kbd_ch(pm, pm->x, pm->y); + if (ch == KBD_SPACE) + ch = ' '; + /* check for hangul input */ if (ch >= 0x3131 && ch <= 0x3163) { @@ -1552,6 +1685,18 @@ static void kbd_move_picker_horizontal(struct keyboard_parameters *pm, state->changed = CHANGED_PICKER; pm->x += dir; + /* The space bar spans several cells but highlights and inserts as one key, + * so step over the whole run instead of stopping inside it, which looks + * like the cursor is stuck. */ + if (pm->x >= 0 && pm->x < pm->max_chars && + get_kbd_ch(pm, pm->x, pm->y) == KBD_SPACE && + get_kbd_ch(pm, pm->x - dir, pm->y) == KBD_SPACE) + { + while (pm->x >= 0 && pm->x < pm->max_chars && + get_kbd_ch(pm, pm->x, pm->y) == KBD_SPACE) + pm->x += dir; + } + if (pm->x < 0) { if (!global_settings.list_wraparound && pm->page == 0) diff --git a/firmware/export/config/hibyr1.h b/firmware/export/config/hibyr1.h index bb20b0b1a2..6a24de6e2e 100644 --- a/firmware/export/config/hibyr1.h +++ b/firmware/export/config/hibyr1.h @@ -37,6 +37,8 @@ #ifndef BOOTLOADER #define HAVE_BUTTON_DATA #define HAVE_TOUCHSCREEN +/* Finger-sized keys, tapped directly, instead of a navigated cursor */ +#define HAVE_KBD_POINT_MODE #endif #ifndef CONFIG_BACKLIGHT_FADING diff --git a/firmware/export/config/hibyr3proii.h b/firmware/export/config/hibyr3proii.h index 92d338aee2..b4e2baa6dd 100644 --- a/firmware/export/config/hibyr3proii.h +++ b/firmware/export/config/hibyr3proii.h @@ -37,6 +37,8 @@ #ifndef BOOTLOADER #define HAVE_BUTTON_DATA #define HAVE_TOUCHSCREEN +/* Finger-sized keys, tapped directly, instead of a navigated cursor */ +#define HAVE_KBD_POINT_MODE #endif #ifndef CONFIG_BACKLIGHT_FADING diff --git a/manual/rockbox_interface/browsing_and_playing.tex b/manual/rockbox_interface/browsing_and_playing.tex index ddf632d379..4ea4d65000 100644 --- a/manual/rockbox_interface/browsing_and_playing.tex +++ b/manual/rockbox_interface/browsing_and_playing.tex @@ -211,6 +211,16 @@ The virtual keyboard can be easily changed by making a text file with the required layout. More information on how to achieve this can be found on the Rockbox website at \wikilink{LoadableKeyboardLayouts}. +\opt{kbd_point_mode}{ + On this player the keyboard is laid out for direct touch where the keys are sized + for a fingertip and are tapped individually, rather than being selected + with the 3\,\texttimes\,3 grid used on other touchscreen targets. By default the + characters are spread over three pages with lower case and accented + letters, their upper-case forms in the same positions, and a third page + of punctuation and additional European characters. Tap + \textbf{\textgreater} below the keys to change page. +} + \opt{morse_input}{ Also you can switch to Morse code input mode by changing the \setting{Use Morse Code Input} setting% diff --git a/manual/rockbox_interface/images/ss-virtual-keyboard-480x720x16.png b/manual/rockbox_interface/images/ss-virtual-keyboard-480x720x16.png new file mode 100644 index 0000000000000000000000000000000000000000..9dec07971ac8d3e960dc7ef5e3cf6bea2ad3c0c9 GIT binary patch literal 4028 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 Date: Tue, 25 Aug 2026 10:11:41 -0400 Subject: [PATCH 117/408] =?UTF-8?q?FS#13988=20-=20Hungarian=20translation?= =?UTF-8?q?=20update=20(Gy=C3=BAr=C3=B3czki=20Norbert)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Ic9f376f01b03d2564dbcb348a29e23569a2ebdd3 --- apps/lang/magyar.lang | 198 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 176 insertions(+), 22 deletions(-) diff --git a/apps/lang/magyar.lang b/apps/lang/magyar.lang index 6383dc0480..31a324c04b 100644 --- a/apps/lang/magyar.lang +++ b/apps/lang/magyar.lang @@ -991,10 +991,10 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Csúcsszűrő %d" + *: "Csúcsszűrő" *: "Csúcsszűrő" @@ -15054,20 +15054,6 @@ *: "Nem bővítmény" - - id: LANG_OPEN_PLUGIN_SET_WPS_CONTEXT_PLUGIN - desc: open plugin module - user: core - - *: "Set WPS Context Plugin" - - - *: "WPS Context Plugin beállítása" - - - *: "WPS Context Plugin beállítása" - - id: LANG_PARAMETER desc: @@ -15308,13 +15294,13 @@ id: LANG_VOICED_DATE_FORMAT - desc: format string for how dates will be read back. Y == 4-digit year, A == month name, m == numeric month, d == numeric day. For example, "AdY" will read "January 21 2021" + desc: format string for how dates will be read back. Y == 4-digit year (grouped), y == 4-digit year (numeric), A == month name, m == numeric month, d == numeric day. For example, for 2021-01-05, "AdY" will be voiced as "January 5 twenty twenty-one" and "dmy" will be voiced as "5 1 two thousand twenty one user: core *: "dAY" - *: "dAY" + *: "~dAY" *: "" @@ -15917,16 +15903,16 @@ id: LANG_DEFAULT_BROWSER - desc: in Settings + desc: deprecated user: core - *: "Default Browser" + *: "" - *: "Alapértelmezett böngésző" + *: "" - *: "Alapértelmezett böngésző" + *: "" @@ -16975,3 +16961,171 @@ *: "~U S B" + + id: LANG_SET_CONTEXT_ITEM + desc: WPS context item + user: core + + *: "Set Context Item" + + + *: "Kontextus elem beállítása" + + + *: "Kontextus elem beállítása" + + + + id: LANG_KEEP_DIRECTORY + desc: file browser setting + user: core + + *: "Always remember last folder" + + + *: "Mindig emlékezzen a legutóbbi mappára" + + + *: "Mindig emlékezzen a legutóbbi mappára" + + + + id: LANG_FILE_NOT_FOUND + desc: When file does not exist + user: core + + *: "File not found" + + + *: "Fájl nem található" + + + *: "Fájl nem található" + + + + id: LANG_SHOW_IN_FILES + desc: Reveal item in File Browser + user: core + + *: "Show in Files" + + + *: "Mutatás a Fájlokban" + + + *: "Mutatás a Fájlokban" + + + + id: LANG_CHANNEL_SWAP + desc: in sound_settings + user: core + + *: "Swap Left & Right" + + + *: "Bal & Jobb megcserélése" + + + *: "Bal & Jobb megcserélése" + + + + id: LANG_COUNTDOWN_TIMER_SET + desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration + user: core + + *: "SET TIMER" + + + *: "IDŐZÍTŐ BEÁLLÍTÁSA" + + + *: "Időzítő beállítása" + + + + id: LANG_COUNTDOWN_TIMER_RUNNING + desc: countdown_timer plugin - status label shown while the countdown is active + user: core + + *: "RUNNING" + + + *: "FUT" + + + *: "Fut" + + + + id: LANG_COUNTDOWN_TIMER_PAUSED + desc: countdown_timer plugin - status label shown while the countdown is paused + user: core + + *: "PAUSED" + + + *: "MEGÁLLÍTVA" + + + *: "Megállítva" + + + + id: LANG_COUNTDOWN_TIMER_OVERTIME + desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up + user: core + + *: "OVERTIME" + + + *: "TÚLÓRA" + + + *: "Túlóra" + + + + id: LANG_COUNTDOWN_TIMER_FINISHED + desc: countdown_timer plugin - status label shown at the moment the countdown expires + user: core + + *: "FINISHED" + + + *: "KÉSZ" + + + *: "Kész" + + + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Szűrő %d" + + + *: "Szűrő" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Szűrő típusa" + + + *: "Szűrő típusa" + + From fd2b070bb15055cb791045c08a0e61fb525e7e4f Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 25 Aug 2026 11:09:09 -0400 Subject: [PATCH 118/408] FS#13984 - Rockbox Utility: Add SAPI5 voice volume control (Alessio Lenzi) Rockbox Utility currently exposes SAPI5 voice speed but not the SAPI voice volume. The encoder volume setting is applied after synthesis, so it cannot prevent clipping or distortion already present in the generated wave file. This patch adds a Volume control to the SAPI5 TTS configuration. The value is passed to the standard SpVoice.Volume property before synthesis. Change-Id: Ifa43e55f716f98150e94c86e5e6a6c8bce0ca852 --- tools/sapi_voice.vbs | 5 ++++- utils/rbutilqt/base/rbsettings.cpp | 1 + utils/rbutilqt/base/rbsettings.h | 1 + utils/rbutilqt/base/ttssapi.cpp | 11 +++++++++++ utils/rbutilqt/base/ttssapi.h | 2 ++ 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tools/sapi_voice.vbs b/tools/sapi_voice.vbs index 5bfc820dfa..0242b812f8 100644 --- a/tools/sapi_voice.vbs +++ b/tools/sapi_voice.vbs @@ -39,7 +39,7 @@ Dim oShell, oArgs, oEnv Dim oFSO, oStdIn, oStdOut Dim bVerbose, bList Dim bMSSP -Dim sLanguage, sVoice, sSpeed, sName, sVendor +Dim sLanguage, sVoice, sSpeed, sVolume, sName, sVendor Dim oSpVoice, oSpFS ' SAPI5 voice and filestream Dim oVoice ' for traversing the list of voices @@ -64,6 +64,7 @@ bList = oArgs.Exists("listvoices") sLanguage = oArgs.Item("language") sVoice = oArgs.Item("voice") sSpeed = oArgs.Item("speed") +sVolume = oArgs.Item("volume") ' Create SAPI5 object If bMSSP Then @@ -116,6 +117,8 @@ End If ' Speed selection If sSpeed <> "" Then oSpVoice.Rate = sSpeed +' Volume selection +If sVolume <> "" Then oSpVoice.Volume = sVolume ' Get vendor information, protect from missing attribute sVendor = oSpVoice.Voice.GetAttribute("Vendor") If Err.Number <> 0 Then diff --git a/utils/rbutilqt/base/rbsettings.cpp b/utils/rbutilqt/base/rbsettings.cpp index 4e618ea01c..6cb1353831 100644 --- a/utils/rbutilqt/base/rbsettings.cpp +++ b/utils/rbutilqt/base/rbsettings.cpp @@ -77,6 +77,7 @@ const static struct { { RbSettings::EncoderNarrowBand, ":encoder:/narrowband", "false" }, { RbSettings::WavtrimThreshold, "wavtrimthreshold", "500"}, { RbSettings::TtsSpeed, ":tts:/speed", "175" }, + { RbSettings::TtsVolume, ":tts:/volume", "100" }, { RbSettings::EncoderComplexity, ":encoder:/complexity", "10" }, { RbSettings::EncoderQuality, ":encoder:/quality", "-1.0" }, { RbSettings::EncoderVolume, ":encoder:/volume", "1.0" }, diff --git a/utils/rbutilqt/base/rbsettings.h b/utils/rbutilqt/base/rbsettings.h index a94486da20..cd525d5d5e 100644 --- a/utils/rbutilqt/base/rbsettings.h +++ b/utils/rbutilqt/base/rbsettings.h @@ -69,6 +69,7 @@ class RbSettings : public QObject WavtrimThreshold, EncoderComplexity, TtsSpeed, + TtsVolume, CacheDisabled, EncoderNarrowBand, EncoderQuality, diff --git a/utils/rbutilqt/base/ttssapi.cpp b/utils/rbutilqt/base/ttssapi.cpp index 1ef12cb628..0a612213fa 100644 --- a/utils/rbutilqt/base/ttssapi.cpp +++ b/utils/rbutilqt/base/ttssapi.cpp @@ -29,6 +29,7 @@ TTSSapi::TTSSapi(QObject* parent) : TTSBase(parent) m_TTSTemplate << "/language:%lang"; m_TTSTemplate << "/voice:%voice"; m_TTSTemplate << "/speed:%speed"; + m_TTSTemplate << "/volume:%volume"; m_TTSTemplate << "%options"; m_TTSVoiceTemplate << "//nologo"; @@ -74,6 +75,12 @@ void TTSSapi::generateSettings() speed = 0; insertSetting(eSPEED, new EncTtsSetting(this, EncTtsSetting::eINT, tr("Speed:"), speed, -10, 10)); + // volume + int volume = RbSettings::subValue(m_TTSType, RbSettings::TtsVolume).toInt(); + if(volume > 100 || volume < 0) + volume = 100; + insertSetting(eVOLUME, new EncTtsSetting(this, + EncTtsSetting::eINT, tr("Volume:"), volume, 0, 100)); // options insertSetting(eOPTIONS, new EncTtsSetting(this, EncTtsSetting::eSTRING, tr("Options:"), @@ -90,6 +97,8 @@ void TTSSapi::saveSettings() getSetting(eVOICE)->current().toString()); RbSettings::setSubValue(m_TTSType, RbSettings::TtsSpeed, getSetting(eSPEED)->current().toInt()); + RbSettings::setSubValue(m_TTSType, RbSettings::TtsVolume, + getSetting(eVOLUME)->current().toInt()); RbSettings::setSubValue(m_TTSType, RbSettings::TtsOptions, getSetting(eOPTIONS)->current().toString()); @@ -112,6 +121,7 @@ bool TTSSapi::start(QString *errStr) m_TTSLanguage =RbSettings::subValue(m_TTSType, RbSettings::TtsLanguage).toString(); m_TTSVoice=RbSettings::subValue(m_TTSType, RbSettings::TtsVoice).toString(); m_TTSSpeed=RbSettings::subValue(m_TTSType, RbSettings::TtsSpeed).toString(); + m_TTSVolume=RbSettings::subValue(m_TTSType, RbSettings::TtsVolume).toString(); QFile::remove(QDir::tempPath() +"/sapi_voice.vbs"); QFile::copy(":/builtin/sapi_voice.vbs",QDir::tempPath() + "/sapi_voice.vbs"); @@ -130,6 +140,7 @@ bool TTSSapi::start(QString *errStr) exec.replaceInStrings("%lang",m_TTSLanguage); exec.replaceInStrings("%voice",m_TTSVoice); exec.replaceInStrings("%speed",m_TTSSpeed); + exec.replaceInStrings("%volume",m_TTSVolume); LOG_INFO() << "Start: cscript " << exec; voicescript = new QProcess(nullptr); diff --git a/utils/rbutilqt/base/ttssapi.h b/utils/rbutilqt/base/ttssapi.h index 8ea1370133..4aa8f25c4c 100644 --- a/utils/rbutilqt/base/ttssapi.h +++ b/utils/rbutilqt/base/ttssapi.h @@ -31,6 +31,7 @@ class TTSSapi : public TTSBase eLANGUAGE, eVOICE, eSPEED, + eVOLUME, eOPTIONS }; @@ -64,6 +65,7 @@ class TTSSapi : public TTSBase QString m_TTSLanguage; QString m_TTSVoice; QString m_TTSSpeed; + QString m_TTSVolume; bool m_started; protected: From a8f8aa40b94f553f452af64ce2809058f2db24fd Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Wed, 19 Aug 2026 16:25:36 +0100 Subject: [PATCH 119/408] lastfm_scrobbler: fetch rbversion from plugin API This prevents needlessly rebuilding the plugin whenever RBVERSION changes. (This was introduced by the removal of CVS $Revision$ tags.) Change-Id: Ic981c86319edb9c7eb7f0c1de71d4fbaba8989ac --- apps/plugins/lastfm_scrobbler.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/plugins/lastfm_scrobbler.c b/apps/plugins/lastfm_scrobbler.c index 93b520bee3..e3be350915 100644 --- a/apps/plugins/lastfm_scrobbler.c +++ b/apps/plugins/lastfm_scrobbler.c @@ -87,8 +87,6 @@ Example /* increment this on any code change that effects output */ #define SCROBBLER_VERSION "1.1" -#define SCROBBLER_REVISION " " RBVERSION " " - #define SCROBBLER_BAD_ENTRY "# FAILED - " /* longest entry I've had is 323, add a safety margin */ @@ -531,8 +529,8 @@ static int sbl_check_or_open(bool check_only) /* write file header */ rb->fdprintf(fd, "#AUDIOSCROBBLER/" SCROBBLER_VERSION "\n" "#TZ/UNKNOWN\n" "#CLIENT/Rockbox " - TARGET_NAME SCROBBLER_REVISION - HDR_STR_TIMELESS "\n"); + TARGET_NAME " %s " + HDR_STR_TIMELESS "\n", rb->rbversion); rb->fdprintf(fd, ITEM_HDR); } else From 2be2aeb2a2bafe10fabc6373b1aadee7b03ed5be Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Fri, 26 Jun 2026 20:16:48 +0200 Subject: [PATCH 120/408] 3DS: add circle pad support for scrolling and navigation (Olivier Senn) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map the circle pad to directional buttons, giving an alternative input method to the D-pad. Uses a deadzone of 64 to prevent unintended movement from minor stick drift. 💘 Generated with Crush Assisted-by: Crush:deepseek-v4-pro Edited by Vencislav Atanasov: Fixed KEY_SELECT handling. Change-Id: I34bf7127688423fca209e199d9535e032ad8b753 --- docs/CREDITS | 1 + firmware/target/hosted/ctru/button-ctru.c | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/CREDITS b/docs/CREDITS index 9b64d7dfbc..edb859249c 100644 --- a/docs/CREDITS +++ b/docs/CREDITS @@ -766,6 +766,7 @@ Lê Khánh Bình Michael McAllister Hemant Kumar Jerry Shen +Olivier Senn The libmad team The wavpack team diff --git a/firmware/target/hosted/ctru/button-ctru.c b/firmware/target/hosted/ctru/button-ctru.c index a03c0d0eb5..aaf07f2a83 100644 --- a/firmware/target/hosted/ctru/button-ctru.c +++ b/firmware/target/hosted/ctru/button-ctru.c @@ -86,6 +86,8 @@ void update_sound_slider_level(void) } } +#define CPAD_DEADZONE 64 + int button_read_device(int* data) { int key = BUTTON_NONE; @@ -97,14 +99,26 @@ int button_read_device(int* data) hidScanInput(); u32 kDown = hidKeysDown(); - + if (kDown & KEY_SELECT) { touchscreen_set_mode(touchscreen_get_mode() == TOUCHSCREEN_POINT ? TOUCHSCREEN_BUTTON : TOUCHSCREEN_POINT); printf("Touchscreen mode: %s\n", touchscreen_get_mode() == TOUCHSCREEN_POINT ? "TOUCHSCREEN_POINT" : "TOUCHSCREEN_BUTTON"); } - + u32 kHeld = hidKeysHeld(); + /* Circle pad → directional buttons (alternative to D-pad) */ + circlePosition cpad; + hidCircleRead(&cpad); + if (cpad.dx > CPAD_DEADZONE) + kHeld |= KEY_DRIGHT; + else if (cpad.dx < -CPAD_DEADZONE) + kHeld |= KEY_DLEFT; + if (cpad.dy > CPAD_DEADZONE) + kHeld |= KEY_DUP; + else if (cpad.dy < -CPAD_DEADZONE) + kHeld |= KEY_DDOWN; + /* rockbox will handle button repeats */ kDown |= kHeld; From 0e2a3cc0c67d0f0062adabec602be27fe1bc014e Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Fri, 26 Jun 2026 20:22:08 +0200 Subject: [PATCH 121/408] 3DS: enable software tone controls via DSP (Olivier Senn) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route bass and treble adjustments through Rockbox's DSP pipeline using biquad filters, making the Bass/Treble sliders in Sound Settings functional rather than no-ops. 💘 Generated with Crush Assisted-by: Crush:deepseek-v4-pro Change-Id: I5d168aaa233fe450defaea03d6657732abd2ec47 --- firmware/export/config/ctru.h | 1 + 1 file changed, 1 insertion(+) diff --git a/firmware/export/config/ctru.h b/firmware/export/config/ctru.h index da9c2b3fb3..414c99757c 100644 --- a/firmware/export/config/ctru.h +++ b/firmware/export/config/ctru.h @@ -77,6 +77,7 @@ #define HAVE_CTRU_AUDIO #define HAVE_HEADPHONE_DETECTION +#define HAVE_SW_TONE_CONTROLS /* #define HAVE_SW_VOLUME_CONTROL */ /* #define PCM_SW_VOLUME_UNBUFFERED */ /* #define PCM_SW_VOLUME_FRACBITS (16) */ From 8536d981a87ddff264c935c860950a5f6fe6063b Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Fri, 28 Aug 2026 12:43:16 +0200 Subject: [PATCH 122/408] checkwps: print file name extension instead of "WPS" Change-Id: I5b0a90993dcda293400ac0d805451a5f18bca322 --- tools/checkwps/checkwps.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tools/checkwps/checkwps.c b/tools/checkwps/checkwps.c index 6534fa9eac..9b14c7c747 100644 --- a/tools/checkwps/checkwps.c +++ b/tools/checkwps/checkwps.c @@ -342,7 +342,6 @@ int main(int argc, char **argv) struct wps_data wps={0}; enum screen_type screen = SCREEN_MAIN; enum skinnable_screens skin; - struct screen* wps_screen; /* No arguments -> print the help text * Also print the help text upon -h or --help */ @@ -350,7 +349,7 @@ int main(int argc, char **argv) strcmp(argv[1],"-h") == 0 || strcmp(argv[1],"--help") == 0 ) { - printf("Usage: checkwps [OPTIONS] filename.wps [filename2.wps]...\n"); + printf("Usage: checkwps [OPTIONS] filename.wps [filename2.sbs]...\n"); printf("\nOPTIONS:\n"); printf("\t-v\t\tverbose\n"); printf("\t-vv\t\tmore verbose\n"); @@ -402,18 +401,16 @@ int main(int argc, char **argv) else if (valid > 0) continue; /* skip (unsupported by this target but not an error) */ - wps_screen = &screens[screen]; - res = skin_data_load(skin, screen, &wps, name, true, &stats); if (!res) { - printf("WPS parsing failure\n"); + printf("%s parsing failure\n", ext); skin_error_format_message(); ret = 3; goto done; } - printf("WPS parsed OK\n\n"); + printf("%s parsed OK\n\n", ext); if (wps_verbose_level>2) skin_debug_tree(SKINOFFSETTOPTR(skin_buffer, wps.tree)); } From 70fd3e1f1e5d0d9ea0ef1733be6fc9592248048c Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Fri, 28 Aug 2026 12:41:17 +0200 Subject: [PATCH 123/408] ipodcolor: Fall back to UDMA 1 The ipod4g target has already had UDMA 2 disabled (see commits 7d78503 and d118f47) due to reported instabilities. The iPod color, using stock hardware, appears to be affected too. Change-Id: I9aea2efce3026938f719da4e5372b233ff6234cf --- firmware/target/arm/pp/ata-target.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firmware/target/arm/pp/ata-target.h b/firmware/target/arm/pp/ata-target.h index fb92900616..da0ba3adcf 100644 --- a/firmware/target/arm/pp/ata-target.h +++ b/firmware/target/arm/pp/ata-target.h @@ -68,9 +68,9 @@ * UDMA 2 is stable at 30 Mhz. * UDMA 1 is stable at 24 Mhz. * - * A slower mode is used on iPod 4Gs due to reported instabilities. + * A slower mode is used on iPod 4G & color due to reported instabilities. */ -#if CPUFREQ_NORMAL >= 30000000 && !defined(IPOD_4G) +#if CPUFREQ_NORMAL >= 30000000 && !defined(IPOD_4G) && !defined(IPOD_COLOR) #define ATA_MAX_UDMA 2 #elif CPUFREQ_NORMAL >= 24000000 #define ATA_MAX_UDMA 1 From 28e5a125ab50d6a5205e42d01411976c563cb5ab Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Fri, 28 Aug 2026 13:24:08 +0200 Subject: [PATCH 124/408] configure: macos: fix checkwps error messages Change-Id: Ic42c20bf011861413245b671a79de6df1b6f43fb --- tools/configure | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/configure b/tools/configure index bc43bf184b..08ceb746ba 100755 --- a/tools/configure +++ b/tools/configure @@ -4477,7 +4477,10 @@ fi gccver=`$CC -dumpversion`; # figure out the binutil version too -if [ "yes" = "$simulator" ] || [ "yes" = "$application" ] && [ $uname = "Darwin" ]; then +if [ $uname = "Darwin" ] && \ + [ "$app_type" = "sdl-sim" -o \ + "$app_type" = "sdl-app" -o \ + "$app_type" = "checkwps" ]; then ldnum=0 else ldnum=`$LD --version | sed -n '1p' | sed -e 's/.* \([0-9]*\)\.\([0-9]*\).*/\1\2/'` From f40cae6cdc89103ca9432fe1c98aec30bd73cfbf Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Tue, 18 Aug 2026 15:36:41 +0100 Subject: [PATCH 125/408] x1000: rewrite SD driver using sdmmc_host Remove all SD protocol handling and all target specific code like GPIO/interrupt handling and clock parent setup. This can now be handled from sdmmc_host_target_init() for each target. Now only the clock frequency is managed by the MSC driver. To make this code easier to factor out later, it's confined to helper functions that do not access the driver state. One small change is that MSCxDIV output is now clamped to a minimum of 50MHz to avoid unnecessary frequency changes. The MSC_CLKRT divider can still divide 50MHz down to 400 KHz so there is no downside to this. Auto-CMD12 is now unused. Using it would make error handling more difficult for sdmmc_host since the controller does not expose response data for the auto-CMD12. Explicit CMD12 was not handled correctly in the old version of the driver because the busy signal was ignored for R1b responses if there was no associated data transfer. This is now fixed by waiting for the PRG_DONE interrupt instead of END_CMD_RES for non-data transfer R1b type commands. Since existing X1000 targets are all very similar they use a shared implementation in sdmmc-x1000-common.c for clock setup and card detection. New targets can either use this or create a separate file if they are different enough to warrant one. Change-Id: I35396637325d7c06a10151bb6aee64cabdc7b682 --- firmware/SOURCES | 6 +- firmware/export/config/erosqnative.h | 3 + firmware/export/config/fiiom3k.h | 3 + firmware/export/config/shanlingq1.h | 3 + .../target/mips/ingenic_x1000/msc-x1000.c | 1102 +++++------------ .../target/mips/ingenic_x1000/msc-x1000.h | 185 +-- firmware/target/mips/ingenic_x1000/sd-x1000.c | 281 ----- .../mips/ingenic_x1000/sdmmc-x1000-common.c | 116 ++ 8 files changed, 444 insertions(+), 1255 deletions(-) delete mode 100644 firmware/target/mips/ingenic_x1000/sd-x1000.c create mode 100644 firmware/target/mips/ingenic_x1000/sdmmc-x1000-common.c diff --git a/firmware/SOURCES b/firmware/SOURCES index 1a064f5c77..08d81c6edc 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1777,9 +1777,6 @@ target/mips/ingenic_x1000/usb-x1000.c #if (CONFIG_STORAGE & (STORAGE_SD|STORAGE_MMC|STORAGE_ATA)) target/mips/ingenic_x1000/msc-x1000.c #endif -#if (CONFIG_STORAGE & STORAGE_SD) -target/mips/ingenic_x1000/sd-x1000.c -#endif #ifdef BOOTLOADER target/mips/ingenic_x1000/installer-x1000.c target/mips/ingenic_x1000/spl-start.S @@ -1821,6 +1818,7 @@ target/mips/ingenic_x1000/fiiom3k/backlight-fiiom3k.c target/mips/ingenic_x1000/fiiom3k/button-fiiom3k.c target/mips/ingenic_x1000/fiiom3k/lcd-fiiom3k.c target/mips/ingenic_x1000/fiiom3k/power-fiiom3k.c +target/mips/ingenic_x1000/sdmmc-x1000-common.c target/mips/ingenic_x1000/spl-nand-x1000.c #endif /* FIIO_M3K */ @@ -1830,6 +1828,7 @@ target/mips/ingenic_x1000/shanlingq1/backlight-shanlingq1.c target/mips/ingenic_x1000/shanlingq1/button-shanlingq1.c target/mips/ingenic_x1000/shanlingq1/lcd-shanlingq1.c target/mips/ingenic_x1000/shanlingq1/power-shanlingq1.c +target/mips/ingenic_x1000/sdmmc-x1000-common.c target/mips/ingenic_x1000/spl-nand-x1000.c #endif /* SHANLING_Q1 */ @@ -1839,6 +1838,7 @@ target/mips/ingenic_x1000/erosqnative/backlight-erosqnative.c target/mips/ingenic_x1000/erosqnative/button-erosqnative.c target/mips/ingenic_x1000/erosqnative/lcd-erosqnative.c target/mips/ingenic_x1000/erosqnative/power-erosqnative.c +target/mips/ingenic_x1000/sdmmc-x1000-common.c target/mips/ingenic_x1000/spl-nand-x1000.c #endif /* EROS_QN */ diff --git a/firmware/export/config/erosqnative.h b/firmware/export/config/erosqnative.h index cfba1b8ff2..e90670fac2 100644 --- a/firmware/export/config/erosqnative.h +++ b/firmware/export/config/erosqnative.h @@ -95,6 +95,9 @@ #define STORAGE_WANTS_ALIGN #define STORAGE_NEEDS_BOUNCE_BUFFER +/* One SD card slot */ +#define SDMMC_HOST_NUM_SD_CONTROLLERS 1 + /* Power management */ #define CONFIG_BATTERY_MEASURE (VOLTAGE_MEASURE | PERCENTAGE_MEASURE/*|CURRENT_MEASURE*/) #define CONFIG_CHARGING CHARGING_MONITOR diff --git a/firmware/export/config/fiiom3k.h b/firmware/export/config/fiiom3k.h index baf633be70..1093d91a31 100644 --- a/firmware/export/config/fiiom3k.h +++ b/firmware/export/config/fiiom3k.h @@ -84,6 +84,9 @@ #define STORAGE_WANTS_ALIGN #define STORAGE_NEEDS_BOUNCE_BUFFER +/* One SD card slot */ +#define SDMMC_HOST_NUM_SD_CONTROLLERS 1 + /* RTC settings */ #define CONFIG_RTC RTC_X1000 /* TODO: implement HAVE_RTC_ALARM */ diff --git a/firmware/export/config/shanlingq1.h b/firmware/export/config/shanlingq1.h index 289f090187..6b8ef7fb16 100644 --- a/firmware/export/config/shanlingq1.h +++ b/firmware/export/config/shanlingq1.h @@ -67,6 +67,9 @@ #define STORAGE_WANTS_ALIGN #define STORAGE_NEEDS_BOUNCE_BUFFER +/* One SD card slot */ +#define SDMMC_HOST_NUM_SD_CONTROLLERS 1 + /* RTC settings */ #define CONFIG_RTC RTC_X1000 /* TODO: implement HAVE_RTC_ALARM */ diff --git a/firmware/target/mips/ingenic_x1000/msc-x1000.c b/firmware/target/mips/ingenic_x1000/msc-x1000.c index 0ce6ff4990..37ea53340d 100644 --- a/firmware/target/mips/ingenic_x1000/msc-x1000.c +++ b/firmware/target/mips/ingenic_x1000/msc-x1000.c @@ -6,7 +6,7 @@ * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * - * Copyright (C) 2021 Aidan MacDonald + * Copyright (C) 2021-2026 Aidan MacDonald * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -17,7 +17,6 @@ * KIND, either express or implied. * ****************************************************************************/ - #include "system.h" #include "panic.h" #include "led.h" @@ -30,901 +29,368 @@ #include #include -/* #define LOGF_ENABLE */ -#include "logf.h" - -/* TODO - this needs some auditing to better handle errors - * - * There should be a clearer code path involving errors. Especially we should - * ensure that removing the card always resets the driver to a sane state. +/* + * TODO: Clock helpers could be factored out of this file */ -#define DEBOUNCE_TIME (HZ/10) - -static const msc_config msc_configs[] = { -#if defined(FIIO_M3K) -#define MSC_CLOCK_SOURCE X1000_CLK_SCLK_A - { - .msc_nr = 0, - .msc_type = MSC_TYPE_SD, - .bus_width = 4, - .label = "microSD", - .cd_gpio = GPIO_MSC0_CD, - .cd_active_level = 0, - }, -#elif defined(SHANLING_Q1) -#define MSC_CLOCK_SOURCE X1000_CLK_MPLL - { - .msc_nr = 0, - .msc_type = MSC_TYPE_SD, - .bus_width = 4, - .label = "microSD", - .cd_gpio = GPIO_MSC0_CD, - .cd_active_level = 0, - }, - /* NOTE: SDIO wifi card is on msc1 */ -#elif defined(EROS_QN) -#define MSC_CLOCK_SOURCE X1000_CLK_SCLK_A - { - .msc_nr = 0, - .msc_type = MSC_TYPE_SD, - .bus_width = 4, - .label = "microSD", - .cd_gpio = GPIO_MSC0_CD, - .cd_active_level = 0, - }, -#else -# error "Please add X1000 MSC config" -#endif - {.msc_nr = -1}, -}; - -static const msc_config* msc_lookup_config(int msc) +static uint32_t get_msc_clkgr_bit(int msc) { - for(int i = 0; i < MSC_COUNT; ++i) - if(msc_configs[i].msc_nr == msc) - return &msc_configs[i]; - return NULL; + if (msc == 0) + return BM_CPM_CLKGR_MSC0; + else + return BM_CPM_CLKGR_MSC1; } -static msc_drv msc_drivers[MSC_COUNT]; - -static void msc0_cd_interrupt(void); -static void msc1_cd_interrupt(void); - -/* --------------------------------------------------------------------------- - * Initialization - */ +static bool msc_is_gated(int msc) +{ + return REG_CPM_CLKGR & get_msc_clkgr_bit(msc); +} static void msc_gate_clock(int msc, bool gate) { - int bit; - if(msc == 0) - bit = BM_CPM_CLKGR_MSC0; + if (gate) + REG_CPM_CLKGR |= get_msc_clkgr_bit(msc); else - bit = BM_CPM_CLKGR_MSC1; - - if(gate) - REG_CPM_CLKGR |= bit; - else - REG_CPM_CLKGR &= ~bit; + REG_CPM_CLKGR &= ~get_msc_clkgr_bit(msc); } -static void msc_init_one(msc_drv* d, int msc) +static void msc_set_clock(int msc, uint32_t src_freq, uint32_t bus_freq) { - /* Lookup config */ - d->drive_nr = -1; - d->config = msc_lookup_config(msc); - if(!d->config) { - d->msc_nr = -1; - return; - } - - /* Initialize driver state */ - d->msc_nr = msc; - d->driver_flags = 0; - d->clk_status = 0; - d->cmdat_def = jz_orf(MSC_CMDAT, RTRG_V(GE32), TTRG_V(LE32)); - d->req = NULL; - d->iflag_done = 0; - d->card_present = 1; - d->card_present_last = 1; - d->req_running = 0; - mutex_init(&d->lock); - semaphore_init(&d->cmd_done, 1, 0); - - /* Ensure correct clock source */ - jz_writef(CPM_MSC0CDR, CE(1), CLKDIV(0), - CLKSRC(MSC_CLOCK_SOURCE == X1000_CLK_MPLL ? 1 : 0)); - while(jz_readf(CPM_MSC0CDR, BUSY)); - jz_writef(CPM_MSC0CDR, CE(0)); - - /* Initialize the hardware */ - msc_gate_clock(msc, false); - msc_full_reset(d); - system_enable_irq(msc == 0 ? IRQ_MSC0 : IRQ_MSC1); - - /* Setup the card detect IRQ */ - if(d->config->cd_gpio != GPIO_NONE) { - if(gpio_get_level(d->config->cd_gpio) != d->config->cd_active_level) { - d->card_present = 0; - d->card_present_last = 0; - } - - system_set_irq_handler(GPIO_TO_IRQ(d->config->cd_gpio), - msc == 0 ? msc0_cd_interrupt : msc1_cd_interrupt); - gpio_set_function(d->config->cd_gpio, GPIOF_IRQ_EDGE(1)); - gpio_flip_edge_irq(d->config->cd_gpio); - gpio_enable_irq(d->config->cd_gpio); - } -} - -void msc_init(void) -{ - /* Only do this once -- each storage subsystem calls us in its init */ - static bool done = false; - if(done) - return; - done = true; - - /* Set up each MSC driver according to msc_configs */ - for(int i = 0; i < MSC_COUNT; ++i) - msc_init_one(&msc_drivers[i], i); -} - -msc_drv* msc_get(int type, int index) -{ - for(int i = 0, m = 0; i < MSC_COUNT; ++i) { - if(msc_drivers[i].config == NULL) - continue; - if(type == MSC_TYPE_ANY || msc_drivers[i].config->msc_type == type) - if(index == m++) - return &msc_drivers[i]; - } - - return NULL; -} - -msc_drv* msc_get_by_drive(int drive_nr) -{ - for(int i = 0; i < MSC_COUNT; ++i) - if(msc_drivers[i].drive_nr == drive_nr) - return &msc_drivers[i]; - return NULL; -} - -void msc_lock(msc_drv* d) -{ - mutex_lock(&d->lock); -} - -void msc_unlock(msc_drv* d) -{ - mutex_unlock(&d->lock); -} - -void msc_full_reset(msc_drv* d) -{ - msc_lock(d); - msc_set_clock_mode(d, MSC_CLK_AUTOMATIC); - msc_set_speed(d, MSC_SPEED_INIT); - msc_set_width(d, 1); - msc_ctl_reset(d); - d->driver_flags = 0; - memset(&d->cardinfo, 0, sizeof(tCardInfo)); - msc_unlock(d); -} - -bool msc_card_detect(msc_drv* d) -{ - if(d->config->cd_gpio == GPIO_NONE) - return true; - - return gpio_get_level(d->config->cd_gpio) == d->config->cd_active_level; -} - -void msc_led_trigger(void) -{ - bool state = false; - for(int i = 0; i < MSC_COUNT; ++i) - if(msc_drivers[i].req_running) - state = true; - - led(state); -} - -/* --------------------------------------------------------------------------- - * Controller API - */ - -void msc_ctl_reset(msc_drv* d) -{ - /* Ingenic code suggests a reset changes clkrt */ - int clkrt = REG_MSC_CLKRT(d->msc_nr); - - /* Send reset -- bit is NOT self clearing */ - jz_overwritef(MSC_CTRL(d->msc_nr), RESET(1)); - udelay(100); - jz_writef(MSC_CTRL(d->msc_nr), RESET(0)); - - /* Verify reset in the status register */ - long deadline = current_tick + HZ; - while(jz_readf(MSC_STAT(d->msc_nr), IS_RESETTING) && - current_tick < deadline) { - sleep(1); - } - - /* Ensure the clock state is as expected */ - if(d->clk_status & MSC_CLKST_AUTO) - jz_writef(MSC_LPM(d->msc_nr), ENABLE(1)); - else if(d->clk_status & MSC_CLKST_ENABLE) - jz_overwritef(MSC_CTRL(d->msc_nr), CLOCK_V(START)); - else - jz_overwritef(MSC_CTRL(d->msc_nr), CLOCK_V(STOP)); - - /* Clear and mask interrupts */ - REG_MSC_IMASK(d->msc_nr) = 0xffffffff; - REG_MSC_IFLAG(d->msc_nr) = 0xffffffff; - - /* Restore clkrt */ - REG_MSC_CLKRT(d->msc_nr) = clkrt; -} - -void msc_set_clock_mode(msc_drv* d, int mode) -{ - int cur_mode = (d->clk_status & MSC_CLKST_AUTO) ? MSC_CLK_AUTOMATIC - : MSC_CLK_MANUAL; - if(mode == cur_mode) - return; - - d->clk_status &= ~MSC_CLKST_ENABLE; - if(mode == MSC_CLK_AUTOMATIC) { - d->clk_status |= MSC_CLKST_AUTO; - jz_writef(MSC_CTRL(d->msc_nr), CLOCK_V(STOP)); - jz_writef(MSC_LPM(d->msc_nr), ENABLE(1)); - } else { - d->clk_status &= ~MSC_CLKST_AUTO; - jz_writef(MSC_LPM(d->msc_nr), ENABLE(0)); - jz_writef(MSC_CTRL(d->msc_nr), CLOCK_V(STOP)); - } -} - -void msc_enable_clock(msc_drv* d, bool enable) -{ - if(d->clk_status & MSC_CLKST_AUTO) - return; - - bool is_enabled = (d->clk_status & MSC_CLKST_ENABLE); - if(enable == is_enabled) - return; - - if(enable) { - jz_writef(MSC_CTRL(d->msc_nr), CLOCK_V(START)); - d->clk_status |= MSC_CLKST_ENABLE; - } else { - jz_writef(MSC_CTRL(d->msc_nr), CLOCK_V(STOP)); - d->clk_status &= ~MSC_CLKST_ENABLE; - } -} - -void msc_set_speed(msc_drv* d, int rate) -{ - /* Shut down clock while we change frequencies */ - if(d->clk_status & MSC_CLKST_ENABLE) - jz_writef(MSC_CTRL(d->msc_nr), CLOCK_V(STOP)); - - /* Wait for clock to go idle */ - while(jz_readf(MSC_STAT(d->msc_nr), CLOCK_EN)) + /* Wait for clock to go idle from any ongoing operation */ + while (jz_readf(MSC_STAT(msc), CLOCK_EN)) sleep(1); - /* freq1 is output by MSCxDIV; freq2 is output by MSC_CLKRT */ - uint32_t freq1 = rate; - uint32_t freq2 = rate; - if(freq1 < MSC_SPEED_FAST) - freq1 = MSC_SPEED_FAST; - - /* Handle MSCxDIV */ - uint32_t src_freq = clk_get(MSC_CLOCK_SOURCE) / 2; - uint32_t div = clk_calc_div(src_freq, freq1); - if(d->msc_nr == 0) { + /* + * Handle MSCxDIV, the max CLKRT division factor is 1/128 + * which allows us to get a 400 KHz clock from up to 50 MHz + * so there is no point using a lower MSCxDIV clock. + */ + uint32_t mscdiv_freq = MAX(bus_freq, 50000000); + uint32_t div = clk_calc_div(src_freq / 2, mscdiv_freq); + if (msc == 0) + { jz_writef(CPM_MSC0CDR, CE(1), CLKDIV(div - 1)); while(jz_readf(CPM_MSC0CDR, BUSY)); jz_writef(CPM_MSC0CDR, CE(0)); - } else { + } + else + { jz_writef(CPM_MSC1CDR, CE(1), CLKDIV(div - 1)); while(jz_readf(CPM_MSC1CDR, BUSY)); jz_writef(CPM_MSC1CDR, CE(0)); } /* Handle MSC_CLKRT */ - uint32_t clkrt = clk_calc_shift(src_freq/div, freq2); - REG_MSC_CLKRT(d->msc_nr) = clkrt; + uint32_t clkrt = clk_calc_shift(src_freq / (2 * div), bus_freq); + REG_MSC_CLKRT(msc) = clkrt; - /* Handle frequency dependent timing settings + /* + * Handle frequency dependent timing settings * TODO - these settings might be SD specific... */ - uint32_t out_freq = (src_freq/div) >> clkrt; - if(out_freq > MSC_SPEED_FAST) { - jz_writef(MSC_LPM(d->msc_nr), + uint32_t out_freq = (src_freq / (2 * div)) >> clkrt; + if (out_freq > 25000000) + { + jz_writef(MSC_LPM(msc), DRV_SEL_V(RISE_EDGE_DELAY_QTR_PHASE), SMP_SEL_V(RISE_EDGE_DELAYED)); - jz_writef(MSC_CTRL2(d->msc_nr), SPEED_V(HIGHSPEED)); - } else { - jz_writef(MSC_LPM(d->msc_nr), + jz_writef(MSC_CTRL2(msc), SPEED_V(HIGHSPEED)); + } + else + { + jz_writef(MSC_LPM(msc), DRV_SEL_V(FALL_EDGE), SMP_SEL_V(RISE_EDGE)); - jz_writef(MSC_CTRL2(d->msc_nr), SPEED_V(DEFAULT)); + jz_writef(MSC_CTRL2(msc), SPEED_V(DEFAULT)); } - - /* Restart clock if it was running before */ - if(d->clk_status & MSC_CLKST_ENABLE) - jz_writef(MSC_CTRL(d->msc_nr), CLOCK_V(START)); } -void msc_set_width(msc_drv* d, int width) +void x1000_msc_init(struct x1000_msc_controller* ctl, + struct x1000_msc_dma_desc *dma_desc, + int msc_nr, uint32_t src_clk_freq) { - /* Bus width is controlled per command with MSC_CMDAT. */ - if(width == 8) - jz_vwritef(d->cmdat_def, MSC_CMDAT, BUS_WIDTH_V(8BIT)); - else if(width == 4) - jz_vwritef(d->cmdat_def, MSC_CMDAT, BUS_WIDTH_V(4BIT)); + memset(ctl, 0, sizeof(*ctl)); + + ctl->msc_nr = msc_nr; + ctl->src_clk_freq = src_clk_freq; + ctl->dma_desc = dma_desc; + ctl->cmdat_def = jz_orf(MSC_CMDAT, RTRG_V(GE32), TTRG_V(LE32), INIT(1)); + + semaphore_init(&ctl->sem, 1, 0); + + /* Start off gated, sdmmc_host will power us on */ + msc_gate_clock(ctl->msc_nr, true); +} + +void x1000_msc_set_power_enabled(void *controller, bool enabled) +{ + struct x1000_msc_controller *ctl = controller; + + if (enabled) + { + msc_gate_clock(ctl->msc_nr, false); + + /* Controller reset */ + jz_overwritef(MSC_CTRL(ctl->msc_nr), RESET(1)); + udelay(100); + jz_writef(MSC_CTRL(ctl->msc_nr), RESET(0)); + while (jz_readf(MSC_STAT(ctl->msc_nr), IS_RESETTING)); + + /* Ensure interrupt state is clear */ + REG_MSC_IMASK(ctl->msc_nr) = 0xFFFFFFFFu; + REG_MSC_IFLAG(ctl->msc_nr) = 0xFFFFFFFFu; + + /* Set requested bus clock frequency */ + msc_set_clock(ctl->msc_nr, ctl->src_clk_freq, + sdmmc_host_get_bus_freq(ctl->bus_clock)); + + /* Set INIT bit and enable auto clock management */ + ctl->cmdat_def |= BM_MSC_CMDAT_INIT; + jz_writef(MSC_LPM(ctl->msc_nr), ENABLE(1)); + } else - jz_vwritef(d->cmdat_def, MSC_CMDAT, BUS_WIDTH_V(1BIT)); -} + { + /* Disable and gate clock */ + jz_writef(MSC_LPM(ctl->msc_nr), ENABLE(0)); + jz_writef(MSC_CTRL(ctl->msc_nr), CLOCK_V(STOP)); -/* --------------------------------------------------------------------------- - * Request API - */ - -/* Note -- this must only be called with IRQs disabled */ -static void msc_finish_request(msc_drv* d, int status) -{ - REG_MSC_IMASK(d->msc_nr) = 0xffffffff; - REG_MSC_IFLAG(d->msc_nr) = 0xffffffff; - if(d->req->flags & MSC_RF_DATA) - jz_writef(MSC_DMAC(d->msc_nr), ENABLE(0)); - - d->req->status = status; - d->req_running = 0; - d->iflag_done = 0; - - msc_led_trigger(); - timeout_cancel(&d->cmd_tmo); - semaphore_release(&d->cmd_done); -} - -static int msc_req_timeout(struct timeout* tmo) -{ - msc_drv* d = (msc_drv*)tmo->data; - msc_async_abort(d, MSC_REQ_LOCKUP); - return 0; -} - -void msc_async_start(msc_drv* d, msc_req* r) -{ - /* Determined needed cmdat and interrupts */ - unsigned cmdat = d->cmdat_def; - d->iflag_done = jz_orm(MSC_IFLAG, END_CMD_RES); - - cmdat |= jz_orf(MSC_CMDAT, RESP_FMT(r->resptype & ~MSC_RESP_BUSY)); - if(r->resptype & MSC_RESP_BUSY) - cmdat |= jz_orm(MSC_CMDAT, BUSY); - - if(r->flags & MSC_RF_INIT) - cmdat |= jz_orm(MSC_CMDAT, INIT); - - if(r->flags & MSC_RF_DATA) { - cmdat |= jz_orm(MSC_CMDAT, DATA_EN); - if(r->flags & MSC_RF_PROG) - d->iflag_done = jz_orm(MSC_IFLAG, WR_ALL_DONE); - else - d->iflag_done = jz_orm(MSC_IFLAG, DMA_DATA_DONE); + msc_gate_clock(ctl->msc_nr, true); } +} - if(r->flags & MSC_RF_WRITE) - cmdat |= jz_orm(MSC_CMDAT, WRITE_READ); +void x1000_msc_set_bus_width(void *controller, uint32_t width) +{ + struct x1000_msc_controller *ctl = controller; - if(r->flags & MSC_RF_AUTO_CMD12) - cmdat |= jz_orm(MSC_CMDAT, AUTO_CMD12); + switch (width) + { + case SDMMC_BUS_WIDTH_1BIT: + jz_vwritef(ctl->cmdat_def, MSC_CMDAT, BUS_WIDTH_V(1BIT)); + break; - if(r->flags & MSC_RF_ABORT) - cmdat |= jz_orm(MSC_CMDAT, IO_ABORT); + case SDMMC_BUS_WIDTH_4BIT: + jz_vwritef(ctl->cmdat_def, MSC_CMDAT, BUS_WIDTH_V(4BIT)); + break; - unsigned imask = jz_orm(MSC_IMASK, + case SDMMC_BUS_WIDTH_8BIT: + jz_vwritef(ctl->cmdat_def, MSC_CMDAT, BUS_WIDTH_V(8BIT)); + break; + + default: + panicf("%s", __func__); + } +} + +void x1000_msc_set_bus_clock(void *controller, uint32_t clock) +{ + struct x1000_msc_controller *ctl = controller; + uint32_t bus_freq = sdmmc_host_get_bus_freq(clock); + + ctl->bus_clock = clock; + + if (!msc_is_gated(ctl->msc_nr)) + msc_set_clock(ctl->msc_nr, ctl->src_clk_freq, bus_freq); +} + +int x1000_msc_submit_command(void *controller, + const struct sdmmc_host_command *cmd, + struct sdmmc_host_response *resp) +{ + struct x1000_msc_controller *ctl = controller; + uint32_t cmdat = ctl->cmdat_def; + uint32_t imask = jz_orm(MSC_IMASK, CRC_RES_ERROR, CRC_READ_ERROR, CRC_WRITE_ERROR, TIME_OUT_RES, TIME_OUT_READ, END_CMD_RES); - imask |= d->iflag_done; - /* Program the controller */ - if(r->flags & MSC_RF_DATA) { - REG_MSC_NOB(d->msc_nr) = r->nr_blocks; - REG_MSC_BLKLEN(d->msc_nr) = r->block_len; + void *buff_addr = cmd->buffer; + size_t buff_size = cmd->nr_blocks * cmd->block_len; + + /* INIT is only sent for the first command after power up */ + jz_vwritef(ctl->cmdat_def, MSC_CMDAT, INIT(0)); + + /* Response type setting */ + if (cmd->flags & SDMMC_RESP_BUSY) + jz_vwritef(cmdat, MSC_CMDAT, BUSY(1)); + + switch (SDMMC_RESP_LENGTH(cmd->flags)) + { + case SDMMC_RESP_NONE: + jz_vwritef(cmdat, MSC_CMDAT, RESP_FMT(0)); + + ctl->resp_len = 0; + break; + + case SDMMC_RESP_SHORT: + if (cmd->flags & SDMMC_RESP_NOCRC) + jz_vwritef(cmdat, MSC_CMDAT, RESP_FMT(3)); + else + jz_vwritef(cmdat, MSC_CMDAT, RESP_FMT(1)); + + ctl->resp_len = 1; + break; + + case SDMMC_RESP_LONG: + jz_vwritef(cmdat, MSC_CMDAT, RESP_FMT(2)); + ctl->resp_len = 4; + break; + + default: + panicf("%s", __func__); + break; } - REG_MSC_CMD(d->msc_nr) = r->command; - REG_MSC_ARG(d->msc_nr) = r->argument; - REG_MSC_CMDAT(d->msc_nr) = cmdat; + /* Data transfer setup */ + if (SDMMC_DATA_PRESENT(cmd->flags)) + { + if ((uintptr_t)buff_addr & (CACHEALIGN_SIZE - 1)) + panicf("%s: unaligned buffer", __func__); - REG_MSC_IFLAG(d->msc_nr) = imask; - REG_MSC_IMASK(d->msc_nr) &= ~imask; + ctl->dma_desc->nda = 0; + ctl->dma_desc->mem = PHYSADDR(buff_addr); + ctl->dma_desc->len = buff_size; + ctl->dma_desc->cmd = 2; /* ID=0, ENDI=1, LINK=0 */ + commit_dcache_range(ctl->dma_desc, sizeof(*ctl->dma_desc)); - if(r->flags & MSC_RF_DATA) { - d->dma_desc.nda = 0; - d->dma_desc.mem = PHYSADDR(r->data); - d->dma_desc.len = r->nr_blocks * r->block_len; - d->dma_desc.cmd = 2; /* ID=0, ENDI=1, LINK=0 */ - commit_dcache_range(&d->dma_desc, sizeof(d->dma_desc)); + if (SDMMC_DATA_DIR(cmd->flags) == SDMMC_DATA_WRITE) + { + commit_dcache_range(buff_addr, buff_size); - if(r->flags & MSC_RF_WRITE) - commit_dcache_range(r->data, d->dma_desc.len); + jz_vwritef(cmdat, MSC_CMDAT, WRITE_READ(1)); + ctl->iflag_done = jz_orm(MSC_IMASK, WR_ALL_DONE); + } else - discard_dcache_range(r->data, d->dma_desc.len); + { + discard_dcache_range(buff_addr, buff_size); - /* Unaligned address for DMA doesn't seem to work correctly. - * FAT FS driver should ensure proper alignment of all buffers, - * so in practice this panic should not occur, but if it does - * I want to hear about it. */ - if(UNLIKELY(d->dma_desc.mem & 3)) { - panicf("msc%d bad align: %08x", d->msc_nr, - (unsigned)d->dma_desc.mem); + ctl->iflag_done = jz_orm(MSC_IMASK, DMA_DATA_DONE); } - jz_writef(MSC_DMAC(d->msc_nr), MODE_SEL(0), INCR(0), DMASEL(0)); - REG_MSC_DMANDA(d->msc_nr) = PHYSADDR(&d->dma_desc); + jz_vwritef(cmdat, MSC_CMDAT, DATA_EN(1)); + + jz_writef(MSC_DMAC(ctl->msc_nr), MODE_SEL(0), INCR(0), DMASEL(0)); + REG_MSC_DMANDA(ctl->msc_nr) = PHYSADDR(ctl->dma_desc); + } + else + { + /* + * PROG_DONE is actually waiting for the busy signal so is + * required for all commands with R1b response (like CMD12). + * For writes, the WR_ALL_DONE interrupt subsumes PROG_DONE. + */ + if (cmd->flags & SDMMC_RESP_BUSY) + ctl->iflag_done = jz_orm(MSC_IMASK, PROG_DONE); + else + ctl->iflag_done = jz_orm(MSC_IMASK, END_CMD_RES); } - /* Begin processing */ - d->req = r; - d->req_running = 1; - msc_led_trigger(); - jz_writef(MSC_CTRL(d->msc_nr), START_OP(1)); - if(r->flags & MSC_RF_DATA) - jz_writef(MSC_DMAC(d->msc_nr), ENABLE(1)); + REG_MSC_NOB(ctl->msc_nr) = cmd->nr_blocks; + REG_MSC_BLKLEN(ctl->msc_nr) = cmd->block_len; + REG_MSC_CMD(ctl->msc_nr) = cmd->command; + REG_MSC_ARG(ctl->msc_nr) = cmd->argument; + REG_MSC_CMDAT(ctl->msc_nr) = cmdat; - /* TODO: calculate a suitable lower value for the lockup timeout. - * - * The SD spec defines timings based on the number of blocks transferred, - * see sec. 4.6.2 "Read, write, and erase timeout conditions". This should - * reduce the long delays which happen if errors occur. - * - * Also need to check if registers MSC_RDTO / MSC_RESTO are correctly set. - */ - timeout_register(&d->cmd_tmo, msc_req_timeout, 10*HZ, (intptr_t)d); + imask |= ctl->iflag_done; + REG_MSC_IFLAG(ctl->msc_nr) = imask; + REG_MSC_IMASK(ctl->msc_nr) &= ~imask; + + ctl->resp = resp; + ctl->err_code = SDMMC_STATUS_OK; + membarrier(); + + jz_writef(MSC_CTRL(ctl->msc_nr), START_OP(1)); + if (SDMMC_DATA_PRESENT(cmd->flags)) + jz_writef(MSC_DMAC(ctl->msc_nr), ENABLE(1)); + + semaphore_wait(&ctl->sem, TIMEOUT_BLOCK); + + if (SDMMC_DATA_PRESENT(cmd->flags) && + SDMMC_DATA_DIR(cmd->flags) == SDMMC_DATA_READ) + discard_dcache_range(buff_addr, buff_size); + + return ctl->err_code; } -void msc_async_abort(msc_drv* d, int status) +static void x1000_msc_finish_command(struct x1000_msc_controller *ctl) { + REG_MSC_IMASK(ctl->msc_nr) = 0xFFFFFFFFu; + REG_MSC_IFLAG(ctl->msc_nr) = 0xFFFFFFFFu; + jz_writef(MSC_DMAC(ctl->msc_nr), ENABLE(0)); + semaphore_release(&ctl->sem); +} + +void x1000_msc_abort_command(void *controller) +{ + struct x1000_msc_controller *ctl = controller; int irq = disable_irq_save(); - if(d->req_running) { - logf("msc%d: async abort status:%d", d->msc_nr, status); - msc_finish_request(d, status); - } + + /* + * Looks strange but this wait is always OK because it either + * acquires the semaphore of an idle controller or is a no-op + * if there is a command running. + * + * Finishing the command then releases the semaphore which is + * correct for both cases. + */ + semaphore_wait(&ctl->sem, TIMEOUT_NOBLOCK); + + ctl->err_code = SDMMC_STATUS_ERROR; + x1000_msc_finish_command(ctl); restore_irq(irq); } -int msc_async_wait(msc_drv* d, int timeout) +static void x1000_msc_read_response(struct x1000_msc_controller *ctl) { - if(semaphore_wait(&d->cmd_done, timeout) == OBJ_WAIT_TIMEDOUT) - return MSC_REQ_INCOMPLETE; + uint32_t res = REG_MSC_RES(ctl->msc_nr); + uint32_t dat; - return d->req->status; -} - -int msc_request(msc_drv* d, msc_req* r) -{ - msc_async_start(d, r); - return msc_async_wait(d, TIMEOUT_BLOCK); -} - -/* --------------------------------------------------------------------------- - * Command response handling - */ - -static void msc_read_response(msc_drv* d) -{ - unsigned res = REG_MSC_RES(d->msc_nr); - unsigned dat; - switch(d->req->resptype) { - case MSC_RESP_R1: - case MSC_RESP_R1B: - case MSC_RESP_R3: - case MSC_RESP_R6: - case MSC_RESP_R7: - dat = res << 24; - res = REG_MSC_RES(d->msc_nr); - dat |= res << 8; - res = REG_MSC_RES(d->msc_nr); - dat |= res & 0xff; - d->req->response[0] = dat; - break; - - case MSC_RESP_R2: - for(int i = 0; i < 4; ++i) { + if (ctl->resp_len == 4) + { + for (int i = 0; i < 4; ++i) + { dat = res << 24; - res = REG_MSC_RES(d->msc_nr); + res = REG_MSC_RES(ctl->msc_nr); dat |= res << 8; - res = REG_MSC_RES(d->msc_nr); + res = REG_MSC_RES(ctl->msc_nr); dat |= res >> 8; - d->req->response[i] = dat; - } - break; - - default: - return; - } -} - -static int msc_check_sd_response(msc_drv* d) -{ - if(d->req->resptype == MSC_RESP_R1 || - d->req->resptype == MSC_RESP_R1B) { - if(d->req->response[0] & SD_R1_CARD_ERROR) { - logf("msc%d: R1 card error: %08x", d->msc_nr, d->req->response[0]); - return MSC_REQ_CARD_ERR; + if (ctl->resp) + ctl->resp->data[i] = dat; } } + else if (ctl->resp_len == 1) + { + dat = res << 24; + res = REG_MSC_RES(ctl->msc_nr); + dat |= res << 8; + res = REG_MSC_RES(ctl->msc_nr); + dat |= res & 0xff; - return MSC_REQ_SUCCESS; -} - -static int msc_check_response(msc_drv* d) -{ - switch(d->config->msc_type) { - case MSC_TYPE_SD: - return msc_check_sd_response(d); - default: - /* TODO - implement msc_check_response for MMC and CE-ATA */ - return 0; + if (ctl->resp) + ctl->resp->data[0] = dat; } } -/* --------------------------------------------------------------------------- - * Interrupt handlers - */ - -static void msc_interrupt(msc_drv* d) +void x1000_msc_irq_handler(struct x1000_msc_controller *ctl) { - const unsigned tmo_bits = jz_orm(MSC_IFLAG, TIME_OUT_READ, TIME_OUT_RES); - const unsigned crc_bits = jz_orm(MSC_IFLAG, CRC_RES_ERROR, - CRC_READ_ERROR, CRC_WRITE_ERROR); - const unsigned err_bits = tmo_bits | crc_bits; + const uint32_t tmo_bits = jz_orm(MSC_IFLAG, TIME_OUT_READ, TIME_OUT_RES); + const uint32_t crc_bits = jz_orm(MSC_IFLAG, CRC_RES_ERROR, CRC_READ_ERROR, CRC_WRITE_ERROR); - unsigned iflag = REG_MSC_IFLAG(d->msc_nr) & ~REG_MSC_IMASK(d->msc_nr); - bool handled = false; + uint32_t iflag = REG_MSC_IFLAG(ctl->msc_nr); - /* In case card was removed */ - if(!msc_card_detect(d)) { - msc_finish_request(d, MSC_REQ_EXTRACTED); - return; + if (iflag & tmo_bits) + ctl->err_code = SDMMC_STATUS_TIMEOUT; + else if (iflag & crc_bits) + ctl->err_code = SDMMC_STATUS_INVALID_CRC; + + /* Read and clear command response */ + if (iflag & BM_MSC_IFLAG_END_CMD_RES) + { + REG_MSC_IMASK(ctl->msc_nr) |= BM_MSC_IFLAG_END_CMD_RES; + REG_MSC_IFLAG(ctl->msc_nr) = BM_MSC_IFLAG_END_CMD_RES; + x1000_msc_read_response(ctl); } - /* Check for errors */ - if(iflag & err_bits) { - int st; - if(iflag & crc_bits) - st = MSC_REQ_CRC_ERR; - else if(iflag & tmo_bits) - st = MSC_REQ_TIMEOUT; - else - st = MSC_REQ_ERROR; - - msc_finish_request(d, st); - return; - } - - /* Read and check the command response */ - if(iflag & BM_MSC_IFLAG_END_CMD_RES) { - msc_read_response(d); - int st = msc_check_response(d); - if(st == MSC_REQ_SUCCESS) { - jz_writef(MSC_IMASK(d->msc_nr), END_CMD_RES(1)); - jz_overwritef(MSC_IFLAG(d->msc_nr), END_CMD_RES(1)); - handled = true; - } else { - msc_finish_request(d, st); - return; - } - } - - /* Check if the "done" interrupt is signaled */ - if(iflag & d->iflag_done) { - /* Discard after DMA in case of hardware cache prefetching. - * Only needed for read operations. - */ - if((d->req->flags & MSC_RF_DATA) != 0 && - (d->req->flags & MSC_RF_WRITE) == 0) { - discard_dcache_range(d->req->data, - d->req->block_len * d->req->nr_blocks); - } - - msc_finish_request(d, MSC_REQ_SUCCESS); - return; - } - - if(!handled) { - panicf("msc%d: irq bug! iflag:%08x raw_iflag:%08lx imask:%08lx", - d->msc_nr, iflag, REG_MSC_IFLAG(d->msc_nr), REG_MSC_IMASK(d->msc_nr)); - } -} - -static int msc_cd_callback(struct timeout* tmo) -{ - msc_drv* d = (msc_drv*)tmo->data; - int now_present = msc_card_detect(d) ? 1 : 0; - - /* If the CD pin level changed during the timeout interval, then the - * signal is not yet stable and we need to wait longer. */ - if(now_present != d->card_present_last) { - d->card_present_last = now_present; - return DEBOUNCE_TIME; - } - - /* If there is a change, then broadcast the hotswap event */ - if(now_present != d->card_present) { - if(now_present) { - d->card_present = 1; - queue_broadcast(SYS_HOTSWAP_INSERTED, d->drive_nr); - } else { - msc_async_abort(d, MSC_REQ_EXTRACTED); - d->card_present = 0; - queue_broadcast(SYS_HOTSWAP_EXTRACTED, d->drive_nr); - } - } - - return 0; -} - -static void msc_cd_interrupt(msc_drv* d) -{ - /* Timer to debounce input */ - d->card_present_last = msc_card_detect(d) ? 1 : 0; - timeout_register(&d->cd_tmo, msc_cd_callback, DEBOUNCE_TIME, (intptr_t)d); - - /* Invert the IRQ */ - gpio_flip_edge_irq(d->config->cd_gpio); -} - -void MSC0(void) -{ - msc_interrupt(&msc_drivers[0]); -} - -void MSC1(void) -{ - msc_interrupt(&msc_drivers[1]); -} - -static void msc0_cd_interrupt(void) -{ - msc_cd_interrupt(&msc_drivers[0]); -} - -static void msc1_cd_interrupt(void) -{ - msc_cd_interrupt(&msc_drivers[1]); -} - -/* --------------------------------------------------------------------------- - * SD command helpers - */ - -int msc_cmd_exec(msc_drv* d, msc_req* r) -{ - int status = msc_request(d, r); - if(status == MSC_REQ_SUCCESS) - return status; - else if(status == MSC_REQ_LOCKUP || status == MSC_REQ_EXTRACTED) - d->driver_flags |= MSC_DF_ERRSTATE; - else if(r->flags & (MSC_RF_ERR_CMD12|MSC_RF_AUTO_CMD12)) { - /* After an error, the controller does not automatically issue CMD12, - * so we need to send it if it's needed, as required by the SD spec. - */ - msc_req nreq = {0}; - nreq.command = SD_STOP_TRANSMISSION; - nreq.resptype = MSC_RESP_R1B; - nreq.flags = MSC_RF_ABORT; - logf("msc%d: cmd%d error, sending cmd12", d->msc_nr, r->command); - if(msc_cmd_exec(d, &nreq)) - d->driver_flags |= MSC_DF_ERRSTATE; - } - - logf("msc%d: err:%d, cmd%d, arg:%x", d->msc_nr, status, - r->command, r->argument); - return status; -} - -int msc_app_cmd_exec(msc_drv* d, msc_req* r) -{ - msc_req areq = {0}; - areq.command = SD_APP_CMD; - areq.argument = d->cardinfo.rca; - areq.resptype = MSC_RESP_R1; - if(msc_cmd_exec(d, &areq)) - return areq.status; - - /* Verify that CMD55 was accepted */ - if((areq.response[0] & (1 << 5)) == 0) - return MSC_REQ_ERROR; - - return msc_cmd_exec(d, r); -} - -int msc_cmd_go_idle_state(msc_drv* d) -{ - msc_req req = {0}; - req.command = SD_GO_IDLE_STATE; - req.resptype = MSC_RESP_NONE; - req.flags = MSC_RF_INIT; - return msc_cmd_exec(d, &req); -} - -int msc_cmd_send_if_cond(msc_drv* d) -{ - msc_req req = {0}; - req.command = SD_SEND_IF_COND; - req.argument = 0x1aa; - req.resptype = MSC_RESP_R7; - - /* TODO - Check if SEND_IF_COND timeout is really an error - * IIRC, this can occur if the card isn't HCS (old cards < 2 GiB). - */ - if(msc_cmd_exec(d, &req)) - return req.status; - - /* Set HCS bit if the card responds correctly */ - if((req.response[0] & 0xff) == 0xaa) - d->driver_flags |= MSC_DF_HCS_CARD; - - return MSC_REQ_SUCCESS; -} - -int msc_cmd_app_op_cond(msc_drv* d) -{ - msc_req req = {0}; - req.command = SD_APP_OP_COND; - req.argument = 0x00300000; /* 3.4 - 3.6 V */ - req.resptype = MSC_RESP_R3; - if(d->driver_flags & MSC_DF_HCS_CARD) - req.argument |= (1 << 30); - - int timeout = 2 * HZ; - do { - if(msc_app_cmd_exec(d, &req)) - return req.status; - if(req.response[0] & (1 << 31)) - break; - sleep(1); - } while(--timeout > 0); - - if(timeout == 0) - return MSC_REQ_TIMEOUT; - - return MSC_REQ_SUCCESS; -} - -int msc_cmd_all_send_cid(msc_drv* d) -{ - msc_req req = {0}; - req.command = SD_ALL_SEND_CID; - req.resptype = MSC_RESP_R2; - if(msc_cmd_exec(d, &req)) - return req.status; - - for(int i = 0; i < 4; ++i) - d->cardinfo.cid[i] = req.response[i]; - - return MSC_REQ_SUCCESS; -} - -int msc_cmd_send_rca(msc_drv* d) -{ - msc_req req = {0}; - req.command = SD_SEND_RELATIVE_ADDR; - req.resptype = MSC_RESP_R6; - if(msc_cmd_exec(d, &req)) - return req.status; - - d->cardinfo.rca = req.response[0] & 0xffff0000; - return MSC_REQ_SUCCESS; -} - -int msc_cmd_send_csd(msc_drv* d) -{ - msc_req req = {0}; - req.command = SD_SEND_CSD; - req.argument = d->cardinfo.rca; - req.resptype = MSC_RESP_R2; - if(msc_cmd_exec(d, &req)) - return req.status; - - for(int i = 0; i < 4; ++i) - d->cardinfo.csd[i] = req.response[i]; - sd_parse_csd(&d->cardinfo); - - if(d->cardinfo.sd2plus) - d->driver_flags |= MSC_DF_V2_CARD; - - return 0; -} - -int msc_cmd_select_card(msc_drv* d) -{ - msc_req req = {0}; - req.command = SD_SELECT_CARD; - req.argument = d->cardinfo.rca; - req.resptype = MSC_RESP_R1B; - return msc_cmd_exec(d, &req); -} - -int msc_cmd_set_bus_width(msc_drv* d, int width) -{ - /* TODO - must we check bus width is supported in the cardinfo? */ - msc_req req = {0}; - req.command = SD_SET_BUS_WIDTH; - req.resptype = MSC_RESP_R1; - switch(width) { - case 1: req.argument = 0; break; - case 4: req.argument = 2; break; - default: return MSC_REQ_ERROR; - } - - if(msc_app_cmd_exec(d, &req)) - return req.status; - - msc_set_width(d, width); - return MSC_REQ_SUCCESS; -} - -int msc_cmd_set_clr_card_detect(msc_drv* d, int arg) -{ - msc_req req = {0}; - req.command = SD_SET_CLR_CARD_DETECT; - req.argument = arg; - req.resptype = MSC_RESP_R1; - return msc_app_cmd_exec(d, &req); -} - -int msc_cmd_switch_freq(msc_drv* d) -{ - /* If card doesn't support High Speed, we don't need to send a command */ - if((d->driver_flags & MSC_DF_V2_CARD) == 0) { - msc_set_speed(d, MSC_SPEED_FAST); - return MSC_REQ_SUCCESS; - } - - /* Try switching to High Speed (50 MHz) */ - char buffer[64] CACHEALIGN_ATTR; - msc_req req = {0}; - req.command = SD_SWITCH_FUNC; - req.argument = 0x80fffff1; - req.resptype = MSC_RESP_R1; - req.flags = MSC_RF_DATA; - req.data = &buffer[0]; - req.block_len = 64; - req.nr_blocks = 1; - if(msc_cmd_exec(d, &req)) - return req.status; - - msc_set_speed(d, MSC_SPEED_HIGH); - return MSC_REQ_SUCCESS; -} - -int msc_cmd_send_status(msc_drv* d) -{ - msc_req req = {0}; - req.command = SD_SEND_STATUS; - req.argument = d->cardinfo.rca; - req.resptype = MSC_RESP_R1; - return msc_cmd_exec(d, &req); -} - -int msc_cmd_set_block_len(msc_drv* d, unsigned len) -{ - msc_req req = {0}; - req.command = SD_SET_BLOCKLEN; - req.argument = len; - req.resptype = MSC_RESP_R1; - return msc_cmd_exec(d, &req); + if ((iflag & ctl->iflag_done) || ctl->err_code) + x1000_msc_finish_command(ctl); } diff --git a/firmware/target/mips/ingenic_x1000/msc-x1000.h b/firmware/target/mips/ingenic_x1000/msc-x1000.h index 60f15b749e..aaa7157ee3 100644 --- a/firmware/target/mips/ingenic_x1000/msc-x1000.h +++ b/firmware/target/mips/ingenic_x1000/msc-x1000.h @@ -17,168 +17,47 @@ * KIND, either express or implied. * ****************************************************************************/ - #ifndef __MSC_X1000_H__ #define __MSC_X1000_H__ -#include "kernel.h" -#include "sdmmc.h" -#include +#include "sdmmc_host.h" -/* Number of MSC controllers */ -#define MSC_COUNT 2 +/* Must be allocated on a cacheline boundary */ +struct x1000_msc_dma_desc +{ + uint32_t nda; + uint32_t mem; + uint32_t len; + uint32_t cmd; +}; -/* Media types */ -#define MSC_TYPE_SD 0 -#define MSC_TYPE_MMC 1 -#define MSC_TYPE_ATA 2 -#define MSC_TYPE_ANY 3 - -/* Clock modes */ -#define MSC_CLK_MANUAL 0 -#define MSC_CLK_AUTOMATIC 1 - -/* Clock status bits */ -#define MSC_CLKST_ENABLE (1 << 0) -#define MSC_CLKST_AUTO (1 << 1) - -/* Driver flags */ -#define MSC_DF_ERRSTATE (1 << 0) -#define MSC_DF_READY (1 << 1) -#define MSC_DF_HCS_CARD (1 << 2) -#define MSC_DF_V2_CARD (1 << 3) -#define MSC_DF_HAS_SBC (1 << 4) - -/* Request status codes */ -#define MSC_REQ_SUCCESS 0 -#define MSC_REQ_CRC_ERR 1 -#define MSC_REQ_CARD_ERR 2 -#define MSC_REQ_TIMEOUT 3 -#define MSC_REQ_EXTRACTED 4 -#define MSC_REQ_LOCKUP 5 -#define MSC_REQ_ERROR 6 -#define MSC_REQ_INCOMPLETE (-1) - -/* Response types */ -#define MSC_RESP_NONE 0 -#define MSC_RESP_BUSY (1 << 7) -#define MSC_RESP_R1 1 -#define MSC_RESP_R1B (MSC_RESP_R1|MSC_RESP_BUSY) -#define MSC_RESP_R2 2 -#define MSC_RESP_R3 3 -#define MSC_RESP_R6 6 -#define MSC_RESP_R7 7 - -/* Request flags */ -#define MSC_RF_INIT (1 << 0) -#define MSC_RF_ERR_CMD12 (1 << 1) -#define MSC_RF_AUTO_CMD12 (1 << 2) -#define MSC_RF_PROG (1 << 3) -#define MSC_RF_DATA (1 << 4) -#define MSC_RF_WRITE (1 << 5) -#define MSC_RF_ABORT (1 << 6) - -/* Clock speeds */ -#define MSC_SPEED_INIT 400000 -#define MSC_SPEED_FAST 25000000 -#define MSC_SPEED_HIGH 50000000 - -typedef struct msc_config { +struct x1000_msc_controller +{ int msc_nr; - int msc_type; - int bus_width; - const char* label; - int cd_gpio; - int cd_active_level; -} msc_config; + uint32_t src_clk_freq; + struct x1000_msc_dma_desc *dma_desc; -typedef struct msc_req { - /* Filled by caller */ - int command; - unsigned argument; - int resptype; - int flags; - void* data; - unsigned nr_blocks; - unsigned block_len; + uint32_t bus_clock; + uint32_t cmdat_def; + uint32_t iflag_done; + int resp_len; + int err_code; + struct sdmmc_host_response *resp; - /* Filled by driver */ - volatile unsigned response[4]; - volatile int status; -} msc_req; + struct semaphore sem; +}; -struct sd_dma_desc { - unsigned nda; - unsigned mem; - unsigned len; - unsigned cmd; -} __attribute__((aligned(16))); +void x1000_msc_init(struct x1000_msc_controller* ctl, + struct x1000_msc_dma_desc *dma_desc, + int msc_nr, uint32_t src_clk_freq); -typedef struct msc_drv { - int msc_nr; - int drive_nr; - const msc_config* config; - - int driver_flags; - int clk_status; - unsigned cmdat_def; - msc_req* req; - unsigned iflag_done; - - volatile int req_running; - volatile int card_present; /* Debounced status */ - volatile int card_present_last; /* Status when we last polled it */ - - struct mutex lock; - struct semaphore cmd_done; - struct timeout cmd_tmo; - struct timeout cd_tmo; - struct sd_dma_desc dma_desc; - - tCardInfo cardinfo; -} msc_drv; - -/* Driver initialization, etc */ -extern void msc_init(void); -extern msc_drv* msc_get(int type, int index); -extern msc_drv* msc_get_by_drive(int drive_nr); - -extern void msc_lock(msc_drv* d); -extern void msc_unlock(msc_drv* d); -extern void msc_full_reset(msc_drv* d); -extern bool msc_card_detect(msc_drv* d); - -extern void msc_led_trigger(void); - -/* Controller API */ -extern void msc_ctl_reset(msc_drv* d); -extern void msc_set_clock_mode(msc_drv* d, int mode); -extern void msc_enable_clock(msc_drv* d, bool enable); -extern void msc_set_speed(msc_drv* d, int rate); -extern void msc_set_width(msc_drv* d, int width); - -/* Request API */ -extern void msc_async_start(msc_drv* d, msc_req* r); -extern void msc_async_abort(msc_drv* d, int status); -extern int msc_async_wait(msc_drv* d, int timeout); -extern int msc_request(msc_drv* d, msc_req* r); - -/* Command helpers; note these are written with SD in mind - * and should be reviewed before using them for MMC / CE-ATA - */ -extern int msc_cmd_exec(msc_drv* d, msc_req* r); -extern int msc_app_cmd_exec(msc_drv* d, msc_req* r); -extern int msc_cmd_go_idle_state(msc_drv* d); -extern int msc_cmd_send_if_cond(msc_drv* d); -extern int msc_cmd_app_op_cond(msc_drv* d); -extern int msc_cmd_all_send_cid(msc_drv* d); -extern int msc_cmd_send_rca(msc_drv* d); -extern int msc_cmd_send_csd(msc_drv* d); -extern int msc_cmd_select_card(msc_drv* d); -extern int msc_cmd_set_bus_width(msc_drv* d, int width); -extern int msc_cmd_set_clr_card_detect(msc_drv* d, int arg); -extern int msc_cmd_switch_freq(msc_drv* d); -extern int msc_cmd_send_status(msc_drv* d); -extern int msc_cmd_set_block_len(msc_drv* d, unsigned len); +void x1000_msc_set_power_enabled(void *controller, bool enabled); +void x1000_msc_set_bus_width(void *controller, uint32_t width); +void x1000_msc_set_bus_clock(void *controller, uint32_t clock); +int x1000_msc_submit_command(void *controller, + const struct sdmmc_host_command *cmd, + struct sdmmc_host_response *resp); +void x1000_msc_abort_command(void *controller); +void x1000_msc_irq_handler(struct x1000_msc_controller *ctl); #endif /* __MSC_X1000_H__ */ diff --git a/firmware/target/mips/ingenic_x1000/sd-x1000.c b/firmware/target/mips/ingenic_x1000/sd-x1000.c deleted file mode 100644 index e40db5833d..0000000000 --- a/firmware/target/mips/ingenic_x1000/sd-x1000.c +++ /dev/null @@ -1,281 +0,0 @@ -/*************************************************************************** - * __________ __ ___. - * Open \______ \ ____ ____ | | _\_ |__ _______ ___ - * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / - * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < - * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ - * \/ \/ \/ \/ \/ - * - * Copyright (C) 2021 Aidan MacDonald - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - ****************************************************************************/ - -#include "system.h" -#include "storage.h" -#include "sdmmc.h" -#include "sd.h" -#include "msc-x1000.h" -#include "gpio-x1000.h" -#include - -/* #define LOGF_ENABLE */ -#include "logf.h" - -static msc_drv* sd_to_msc[MSC_COUNT]; -static long _sd_last_disk_activity = 0; - -static void sd_probe_set_block_count(msc_drv* d) -{ - uint8_t scr[CACHEALIGN_SIZE] CACHEALIGN_ATTR; - msc_req req = {0}; - req.command = SD_SEND_SCR; - req.resptype = MSC_RESP_R1; - req.flags = MSC_RF_DATA; - req.data = scr; - req.nr_blocks = 1; - req.block_len = 8; - - if(msc_app_cmd_exec(d, &req) == MSC_REQ_SUCCESS) { - if(scr[3] & 0x02) - d->driver_flags |= MSC_DF_HAS_SBC; - else - d->driver_flags &= ~MSC_DF_HAS_SBC; - } -} - -static int sd_init_card(msc_drv* d) -{ - int s; - if(s = msc_cmd_go_idle_state(d)) return -100 - s; - if(s = msc_cmd_send_if_cond(d)) return -110 - s; - if(s = msc_cmd_app_op_cond(d)) return -120 - s; - if(s = msc_cmd_all_send_cid(d)) return -130 - s; - if(s = msc_cmd_send_rca(d)) return -140 - s; - if(s = msc_cmd_send_csd(d)) return -150 - s; - if(s = msc_cmd_select_card(d)) return -160 - s; - if(s = msc_cmd_set_clr_card_detect(d, 0)) return -170 - s; - if(s = msc_cmd_set_bus_width(d, 4)) return -180 - s; - if(s = msc_cmd_switch_freq(d)) return -190 - s; - sd_probe_set_block_count(d); - d->driver_flags |= MSC_DF_READY; - d->cardinfo.initialized = 1; - return 0; -} - -static int sd_transfer(msc_drv* d, bool write, - sector_t start, int count, void* buf) -{ - int status = -1; - - msc_lock(d); - if(!d->card_present) - goto _exit; - - /* Hopefully puts the driver into a working state */ - if(d->driver_flags & MSC_DF_ERRSTATE) { - logf("MSC%d: attempting to reset after ERRSTATE", d->msc_nr); - msc_full_reset(d); - } - - /* Init card if needed */ - if((d->driver_flags & MSC_DF_READY) == 0) { - if(status = sd_init_card(d)) { - logf("MSC%d: card init failed (code %d)", d->msc_nr, status); - d->driver_flags |= MSC_DF_ERRSTATE; - d->cardinfo.initialized = status; - goto _exit; - } - } - - /* Ensure parameters are within range */ - if(count < 1) - goto _exit; - if(start + count > d->cardinfo.numblocks) - goto _exit; - - do { - /* We can only do 65536 blocks at a time */ - int xfer_count = count > 0xffff ? 0xffff : count; - - /* Set block length. I think this is only necessary for non-HCS cards. - * HCS cards always use 512 bytes so we shouldn't need it. - */ - if((d->driver_flags & MSC_DF_HCS_CARD) == 0) - if(status = msc_cmd_set_block_len(d, SD_BLOCK_SIZE)) - goto _exit; - - bool use_sbc = false; - if(xfer_count > 1 && (d->driver_flags & MSC_DF_HAS_SBC)) { - msc_req sbc_req = {0}; - sbc_req.command = 23; /* CMD23: SET_BLOCK_COUNT (SBC) */ - sbc_req.argument = xfer_count; - sbc_req.resptype = MSC_RESP_R1; - status = msc_cmd_exec(d, &sbc_req); - if(status == MSC_REQ_SUCCESS) { - use_sbc = true; - } else if(status == MSC_REQ_CARD_ERR && - (sbc_req.response[0] & SD_R1_ILLEGAL_COMMAND)) { - d->driver_flags &= ~MSC_DF_HAS_SBC; - } else { - goto _exit; - } - } - - msc_req req = {0}; - req.data = buf; - req.nr_blocks = xfer_count; - req.block_len = SD_BLOCK_SIZE; - req.resptype = MSC_RESP_R1; - req.flags = MSC_RF_DATA; - if(xfer_count > 1) { - if(use_sbc) - req.flags |= MSC_RF_ERR_CMD12; - else - req.flags |= MSC_RF_AUTO_CMD12; - } - if(write) { - req.command = xfer_count == 1 ? SD_WRITE_BLOCK - : SD_WRITE_MULTIPLE_BLOCK; - req.flags |= MSC_RF_PROG | MSC_RF_WRITE; - } else { - req.command = xfer_count == 1 ? SD_READ_SINGLE_BLOCK - : SD_READ_MULTIPLE_BLOCK; - } - - // XXX 64-bit - if(d->driver_flags & MSC_DF_V2_CARD) - req.argument = start; - else - req.argument = start * SD_BLOCK_SIZE; - - if(status = msc_cmd_exec(d, &req)) - goto _exit; - - /* TODO - properly handle reading the last block of the SD card - * This is likely to fail if we're reading near the end because - * the SD card will try to read past the last sector and then - * signal an error. So we need to ignore that error, but only if - * it was expected to occur. (See SD spec sec. 4.3.3, "Block Read") - */ - if(status = msc_cmd_send_status(d)) - goto _exit; - - /* Advance the buffer and adjust start/count */ - buf += xfer_count * SD_BLOCK_SIZE; - start += xfer_count; - count -= xfer_count; - } while(count > 0); - - _exit: - msc_unlock(d); - return status; -} - -int sd_read_sectors(IF_MD(int drive,) sector_t start, - int count, void* buf) -{ - return sd_transfer(sd_to_msc[IF_MD_DRV(drive)], false, - start, count, buf); -} - -int sd_write_sectors(IF_MD(int drive,) sector_t start, - int count, const void* buf) -{ - return sd_transfer(sd_to_msc[IF_MD_DRV(drive)], true, - start, count, (void*)buf); -} - -tCardInfo* card_get_info_target(int card_nr) -{ - /* Defensive measures */ - if(card_nr < 0 || card_nr >= MSC_COUNT || sd_to_msc[card_nr] == NULL) { - static tCardInfo null_info = { 0 }; - return &null_info; - } - - return &sd_to_msc[card_nr]->cardinfo; -} - -int sd_event(long id, intptr_t data) -{ - if(id == SYS_HOTSWAP_EXTRACTED) { - msc_drv* d = msc_get_by_drive(data); - if(d) - msc_full_reset(d); - return 0; - } else { - return storage_event_default_handler(id, data, _sd_last_disk_activity, - STORAGE_SD); - } -} - -long sd_last_disk_activity(void) -{ - return _sd_last_disk_activity; -} - -bool sd_present(IF_MD_NONVOID(int drive)) -{ -#ifndef HAVE_MULTIDRIVE - int drive = 0; -#endif - /* Seems that volume_properties() in firmware/common/disk.c may pass - * drive = -1 when the SD card is not inserted, so just return false. - */ - if(drive < 0) - return false; - - return sd_to_msc[IF_MD_DRV(drive)]->card_present; -} - -bool sd_removable(IF_MD_NONVOID(int drive)) -{ -#ifndef HAVE_MULTIDRIVE - int drive = 0; -#endif - - /* Same reason as sd_present() */ - if(drive < 0) - return false; - - return sd_to_msc[IF_MD_DRV(drive)]->config->cd_gpio != GPIO_NONE; -} - -#ifndef CONFIG_STORAGE_MULTI -static -#endif -int sd_num_drives(int first_drive) -{ - int n = 0; - for(; n < MSC_COUNT; ++n) { - msc_drv* d = msc_get(MSC_TYPE_SD, n); - if(d == NULL) - break; - - d->drive_nr = first_drive + n; - sd_to_msc[n] = d; - } - - for(int i = n; i < MSC_COUNT; ++i) - sd_to_msc[i] = NULL; - - return n; -} - -int sd_init(void) -{ - msc_init(); -#ifndef CONFIG_STORAGE_MULTI - sd_num_drives(0); -#endif - - return 0; -} diff --git a/firmware/target/mips/ingenic_x1000/sdmmc-x1000-common.c b/firmware/target/mips/ingenic_x1000/sdmmc-x1000-common.c new file mode 100644 index 0000000000..ac00a68369 --- /dev/null +++ b/firmware/target/mips/ingenic_x1000/sdmmc-x1000-common.c @@ -0,0 +1,116 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 Aidan MacDonald + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include "sdmmc_host.h" +#include "sdmmc_poll.h" +#include "msc-x1000.h" +#include "gpio-x1000.h" +#include "irq-x1000.h" +#include "clk-x1000.h" + +/* All current X1000 targets use MSC0 */ +#define MSC_NUM 0 + +/* And all targets use an active low card dtect GPIO */ +#define MSC_CD_GPIO GPIO_MSC0_CD +#define MSC_CD_ACTIVE_LEVEL 0 + +/* Q1 uses MPLL, other targets are using SCLK_A */ +#if defined(SHANLING_Q1) +# define MSC_SOURCE_CLOCK X1000_CLK_MPLL +#else +# define MSC_SOURCE_CLOCK X1000_CLK_SCLK_A +#endif + +/* 300ms poll interval */ +#define SDCARD_POLL_TICKS (300 * HZ / 1000) + +static struct sdmmc_host sdmmc; +static struct sdmmc_poll sdmmc_poll; +static struct x1000_msc_controller msc_ctl; +static struct x1000_msc_dma_desc msc_dma_desc; + +static const struct sdmmc_controller_ops sdmmc_ops = { + .set_power_enabled = x1000_msc_set_power_enabled, + .set_bus_width = x1000_msc_set_bus_width, + .set_bus_clock = x1000_msc_set_bus_clock, + .submit_command = x1000_msc_submit_command, + .abort_command = x1000_msc_abort_command, +}; + +static const struct sdmmc_host_config sdmmc_config INITDATA_ATTR = { + .type = STORAGE_SD, + .bus_voltages = SDMMC_BUS_VOLTAGE_3V2_3V3 | + SDMMC_BUS_VOLTAGE_3V3_3V4, + .bus_widths = SDMMC_BUS_WIDTH_1BIT | + SDMMC_BUS_WIDTH_4BIT, + .bus_clocks = SDMMC_BUS_CLOCK_400KHZ | + SDMMC_BUS_CLOCK_25MHZ | + SDMMC_BUS_CLOCK_50MHZ, + .max_nr_blocks = 65535, + .is_removable = true, +}; + +static bool is_sdcard_inserted(void) +{ + return gpio_get_level(MSC_CD_GPIO) == MSC_CD_ACTIVE_LEVEL; +} + +static void sdcard_insert_irq(void) +{ + sdmmc_poll_event(&sdmmc_poll); + gpio_flip_edge_irq(MSC_CD_GPIO); +} + +void sdmmc_host_target_init(void) +{ + /* Configure clock source */ + jz_writef(CPM_MSC0CDR, CE(1), + CLKSRC(MSC_SOURCE_CLOCK == X1000_CLK_MPLL ? 1 : 0)); + while (jz_readf(CPM_MSC0CDR, BUSY)); + jz_writef(CPM_MSC0CDR, CE(0)); + + /* Initialize controller */ + x1000_msc_init(&msc_ctl, &msc_dma_desc, MSC_NUM, clk_get(MSC_SOURCE_CLOCK)); + system_enable_irq(MSC_NUM == 0 ? IRQ_MSC0 : IRQ_MSC1); + + /* Initialize SD/MMC host driver */ + sdmmc_host_init(&sdmmc, &sdmmc_config, &sdmmc_ops, &msc_ctl); + sdmmc_host_init_medium_present(&sdmmc, is_sdcard_inserted()); + + /* Setup card detect handling */ + sdmmc_poll_init(&sdmmc_poll, &sdmmc, is_sdcard_inserted); + + system_set_irq_handler(GPIO_TO_IRQ(MSC_CD_GPIO), sdcard_insert_irq); + gpio_set_function(MSC_CD_GPIO, GPIOF_IRQ_EDGE(1)); + gpio_flip_edge_irq(MSC_CD_GPIO); + gpio_enable_irq(MSC_CD_GPIO); +} + +#if MSC_NUM == 0 +void MSC0(void) +{ + x1000_msc_irq_handler(&msc_ctl); +} +#else +void MSC1(void) +{ + x1000_msc_irq_handler(&msc_ctl); +} +#endif From a95f9debb268a5645bd624ec81a3476133d5efbe Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Sat, 29 Aug 2026 19:08:11 +0100 Subject: [PATCH 126/408] sdmmc_host: move init cleanup into sdmmc_host_device_init() Change-Id: If2fa58602b1c3a9aea4a56c65eb8e1ee37520a2d --- firmware/drivers/sdmmc_host.c | 42 ++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/firmware/drivers/sdmmc_host.c b/firmware/drivers/sdmmc_host.c index 337de92561..95d3f9fb75 100644 --- a/firmware/drivers/sdmmc_host.c +++ b/firmware/drivers/sdmmc_host.c @@ -650,56 +650,56 @@ static int sdmmc_host_device_init(struct sdmmc_host *host) if (rc) { logf("sdmmc_host_cmd_go_idle_state: %d", rc); - return rc; + goto out; } rc = sdmmc_host_cmd_send_if_cond(host); if (rc) { logf("sdmmc_host_cmd_send_if_cond: %d", rc); - return rc; + goto out; } rc = sdmmc_host_cmd_send_app_op_cond(host); if (rc) { logf("sdmmc_host_cmd_send_app_op_cond: %d", rc); - return rc; + goto out; } rc = sdmmc_host_cmd_all_send_cid(host); if (rc) { logf("sdmmc_host_cmd_all_send_cid: %d", rc); - return rc; + goto out; } rc = sdmmc_host_cmd_send_rca(host); if (rc) { logf("sdmmc_host_cmd_send_rca: %d", rc); - return rc; + goto out; } rc = sdmmc_host_cmd_send_csd(host); if (rc) { logf("sdmmc_host_cmd_send_csd: %d", rc); - return rc; + goto out; } rc = sdmmc_host_cmd_select_card(host); if (rc) { logf("sdmmc_host_cmd_select_card: %d", rc); - return rc; + goto out; } rc = sdmmc_host_cmd_clr_card_detect(host); if (rc) { logf("sdmmc_host_cmd_select_card: %d", rc); - return rc; + goto out; } /* @@ -712,7 +712,7 @@ static int sdmmc_host_device_init(struct sdmmc_host *host) if (rc) { logf("sdmmc_host_cmd_set_bus_width: %d", rc); - return rc; + goto out; } sdmmc_host_set_controller_bus_width(host, SDMMC_BUS_WIDTH_4BIT); @@ -729,7 +729,7 @@ static int sdmmc_host_device_init(struct sdmmc_host *host) if (rc) { logf("sdmmc_host_cmd_switch_freq: %d", rc); - return rc; + goto out; } sdmmc_host_set_controller_bus_clock(host, SDMMC_BUS_CLOCK_50MHZ); @@ -743,12 +743,22 @@ static int sdmmc_host_device_init(struct sdmmc_host *host) if (rc) { logf("sdmmc_host_cmd_send_scr: %d", rc); - return rc; + goto out; } - host->initialized = true; - host->cardinfo.initialized = true; - return 0; +out: + if (rc) + { + host->need_reset = true; + } + else + { + host->initialized = true; + host->cardinfo.initialized = true; + } + + sdmmc_host_release_dc_buffer(host); + return rc; } static int sdmmc_host_transfer(struct sdmmc_host *host, @@ -775,12 +785,8 @@ static int sdmmc_host_transfer(struct sdmmc_host *host, if (!host->initialized) { rc = sdmmc_host_device_init(host); - sdmmc_host_release_dc_buffer(host); if (rc) - { - host->need_reset = true; goto out; - } } if (count < 1) From 99c18f336c730f533d3b7d2d8951f5dc4a6da802 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Sat, 29 Aug 2026 19:08:37 +0100 Subject: [PATCH 127/408] sdmmc_host: add quirk for card timeout on single block access Some 512G Samsung cards (and possibly other cards) seem to have problems with repeated single block read commands and sometimes just time out without ever sending the data. The card response is received OK and reports no errors but no data is received. The problem also occurs if a multiple block read is used to transfer a single block but doesn't seem to occur when more than one block is transferred. Less commonly, timeouts can occur on write commands but it is not clear if that happens only after a timed out read. Adding a delay of a few tens of milliseconds before each read/write single block command appears to prevent this. Change-Id: I5a02b71f59cc02832acaf30e5f900a3ee8804e76 --- firmware/drivers/sdmmc_host.c | 21 +++++++++++++++++++++ firmware/export/sdmmc_host.h | 11 +++++++++++ 2 files changed, 32 insertions(+) diff --git a/firmware/drivers/sdmmc_host.c b/firmware/drivers/sdmmc_host.c index 95d3f9fb75..aa95bee9ab 100644 --- a/firmware/drivers/sdmmc_host.c +++ b/firmware/drivers/sdmmc_host.c @@ -221,6 +221,7 @@ static void sdmmc_host_bus_reset(struct sdmmc_host *host) host->initialized = false; host->is_hcs_card = false; host->use_cmd23 = false; + host->quirk_rdwrsingleblock_delay = false; memset(&host->cardinfo, 0, sizeof(host->cardinfo)); } @@ -841,6 +842,9 @@ static int sdmmc_host_transfer(struct sdmmc_host *host, cmd.command = SD_WRITE_BLOCK; else cmd.command = SD_READ_SINGLE_BLOCK; + + if (host->quirk_rdwrsingleblock_delay) + udelay(50); } if (host->cardinfo.sd2plus) @@ -849,6 +853,23 @@ static int sdmmc_host_transfer(struct sdmmc_host *host, cmd.argument = start * SD_BLOCK_SIZE; rc = sdmmc_host_submit_cmd(host, &cmd, NULL); + + if (rc == SDMMC_STATUS_TIMEOUT && + !host->quirk_rdwrsingleblock_delay && + (cmd.command == SD_WRITE_BLOCK || + cmd.command == SD_READ_SINGLE_BLOCK)) + { + logf("sdmmc_host: enabling quirk_rdwrsingleblock_delay"); + + sdmmc_host_bus_reset(host); + rc = sdmmc_host_device_init(host); + if (rc) + goto out; + + host->quirk_rdwrsingleblock_delay = true; + continue; + } + if (rc) goto out; diff --git a/firmware/export/sdmmc_host.h b/firmware/export/sdmmc_host.h index d4a431bbdb..429ff8fa4c 100644 --- a/firmware/export/sdmmc_host.h +++ b/firmware/export/sdmmc_host.h @@ -244,6 +244,17 @@ struct sdmmc_host bool use_cmd23 : 1; bool led_active : 1; /* LED active may be read without lock */ + /* + * Some 512G Samsung cards (and possibly other cards) seem to + * have problems with repeated single block read commands and + * sometimes just time out without ever sending the data. The + * card response is OK but no data is received. + * + * Adding a delay of a few tens of milliseconds before each + * read/write single block command appears to fix the problem. + */ + bool quirk_rdwrsingleblock_delay : 1; + /* Controller implemented by the target */ const struct sdmmc_controller_ops *ops; void *controller; From ce4722985777f492c9497675ea59b571129aeb6a Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Wed, 26 Aug 2026 01:36:46 +0300 Subject: [PATCH 128/408] 3DS: auto-create application data directories on boot (Olivier Senn) Restore paths_init() to create /3ds/.rockbox and the config directory tree at startup, so Rockbox works without manual SD card setup. Edited by Vencislav Atanasov: Use the ROCKBOX_DIR macro instead of hardcoding the paths. Change-Id: I33ab8e588cdca58285d50e9e5698a8071b381cf4 --- firmware/target/hosted/ctru/filesystem-ctru.c | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/firmware/target/hosted/ctru/filesystem-ctru.c b/firmware/target/hosted/ctru/filesystem-ctru.c index 07a3b012de..d7b3688a4e 100644 --- a/firmware/target/hosted/ctru/filesystem-ctru.c +++ b/firmware/target/hosted/ctru/filesystem-ctru.c @@ -37,21 +37,13 @@ void paths_init(void) { - /* is this needed in 3DS? */ -#if 0 - char config_dir[MAX_PATH]; - - const char *home = "/3ds"; - mkdir("/3ds/.rockbox" __MKDIR_MODE_ARG); - - snprintf(config_dir, sizeof(config_dir), "%s/.config", home); - mkdir(config_dir __MKDIR_MODE_ARG); - snprintf(config_dir, sizeof(config_dir), "%s/.config/rockbox.org", home); - mkdir(config_dir __MKDIR_MODE_ARG); - /* Plugin data directory */ - snprintf(config_dir, sizeof(config_dir), "%s/.config/rockbox.org/rocks.data", home); - mkdir(config_dir __MKDIR_MODE_ARG); -#endif + mkdir(ROCKBOX_DIR __MKDIR_MODE_ARG); + mkdir(ROCKBOX_DIR "/backdrops" __MKDIR_MODE_ARG); + mkdir(ROCKBOX_DIR "/eqs" __MKDIR_MODE_ARG); + mkdir(ROCKBOX_DIR "/icons" __MKDIR_MODE_ARG); + mkdir(ROCKBOX_DIR "/rocks.data" __MKDIR_MODE_ARG); + mkdir(ROCKBOX_DIR "/themes" __MKDIR_MODE_ARG); + mkdir(ROCKBOX_DIR "/wps" __MKDIR_MODE_ARG); } /* only sdcard volume is accesible to the user */ From 4d3c7aed0365f4e3ade704773a6cc8d2e276aa5d Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Fri, 26 Jun 2026 21:55:37 +0200 Subject: [PATCH 129/408] 3DS: implement clean power-off via exit(0) (Olivier Senn) Replace the dead infinite loop in power_off() with sim_do_exit() which properly shuts down kernel, timer, mcuhwc, and cfgu services before calling exit(0). Remove the stale sys_poweroff() declaration in system-ctru.h; the generic implementation in powermgmt.c handles SYS_POWEROFF broadcasting. Change-Id: I247c4de482c66c3a060d0b88b7e2239ecec189c5 --- firmware/target/hosted/ctru/system-ctru.c | 12 +++++------- firmware/target/hosted/ctru/system-ctru.h | 1 - 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/firmware/target/hosted/ctru/system-ctru.c b/firmware/target/hosted/ctru/system-ctru.c index d1f760ad32..0a9fb1b2ec 100644 --- a/firmware/target/hosted/ctru/system-ctru.c +++ b/firmware/target/hosted/ctru/system-ctru.c @@ -55,28 +55,26 @@ FS_Archive sdmcArchive; void ctru_sys_quit(void) { - sys_poweroff(); + sim_do_exit(); } void power_off(void) { - /* since sim_thread_shutdown() grabs the mutex we need to let it free, - * otherwise sys_wait_thread will deadlock */ + /* shut down Rockbox kernel threads */ struct thread_entry* t = sim_thread_unlock(); sim_thread_shutdown(); /* lock again before entering the scheduler */ sim_thread_lock(t); - /* sim_thread_shutdown() will cause sim_do_exit() to be called via longjmp, - * but only if we let the sdl thread scheduler exit the other threads */ - while(1) yield(); + sim_do_exit(); } void sim_do_exit() { sim_kernel_shutdown(); sys_timer_quit(); - /* TODO: quit_everything() */ + mcuhwc_close(); + cfguExit(); exit(EXIT_SUCCESS); } diff --git a/firmware/target/hosted/ctru/system-ctru.h b/firmware/target/hosted/ctru/system-ctru.h index fb96af2912..380c671d90 100644 --- a/firmware/target/hosted/ctru/system-ctru.h +++ b/firmware/target/hosted/ctru/system-ctru.h @@ -56,7 +56,6 @@ int set_irq_level(int level); void sim_enter_irq_handler(void); void sim_exit_irq_handler(void); void sim_kernel_shutdown(void); -void sys_poweroff(void); void sys_handle_argv(int argc, char *argv[]); void gui_message_loop(void); void sim_do_exit(void) NORETURN_ATTR; From 9cef2a3aef2034016660f712e90398c0865eb958 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Fri, 26 Jun 2026 22:21:43 +0200 Subject: [PATCH 130/408] 3DS: implement touchscreen_enable_device to gate touch input (Olivier Senn) Change-Id: I2f9d2505b727a6a68e8dba9199ea4053aa902b64 --- firmware/target/hosted/ctru/button-ctru.c | 33 +++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/firmware/target/hosted/ctru/button-ctru.c b/firmware/target/hosted/ctru/button-ctru.c index aaf07f2a83..7c11025fb2 100644 --- a/firmware/target/hosted/ctru/button-ctru.c +++ b/firmware/target/hosted/ctru/button-ctru.c @@ -53,6 +53,8 @@ static enum { STATE_DOWN = 1, } last_touch_state = STATE_UNKNOWN; +static bool touch_enabled = true; + static double map_values(double n, double source_start, double source_end, double dest_start, double dest_end, int decimal_precision ) { double delta_start = source_end - source_start; double delta_end = dest_end - dest_start; @@ -152,22 +154,25 @@ int button_read_device(int* data) } touchPosition touch; - hidTouchRead(&touch); + if (touch_enabled) + { + hidTouchRead(&touch); - /* Generate UP and DOWN events */ - if (kDown & KEY_TOUCH) { - last_touch_state = STATE_DOWN; - } - else { - last_touch_state = STATE_UP; - } + /* Generate UP and DOWN events */ + if (kDown & KEY_TOUCH) { + last_touch_state = STATE_DOWN; + } + else { + last_touch_state = STATE_UP; + } - last_x = touch.px; - last_y = touch.py; + last_x = touch.px; + last_y = touch.py; - int tkey = touchscreen_to_pixels(last_x, last_y, data); - if (last_touch_state == STATE_DOWN) { - key |= tkey; + int tkey = touchscreen_to_pixels(last_x, last_y, data); + if (last_touch_state == STATE_DOWN) { + key |= tkey; + } } update_sound_slider_level(); @@ -183,7 +188,7 @@ void button_init_device(void) #ifndef HAS_BUTTON_HOLD void touchscreen_enable_device(bool en) { - (void)en; + touch_enabled = en; } #endif From 95e9d227aa83d244e87594c6a10412eaea0ee469 Mon Sep 17 00:00:00 2001 From: Sebastian Leonhardt Date: Sun, 30 Aug 2026 14:48:32 +0200 Subject: [PATCH 131/408] manual: fix typos and whitespace; bump copyright to 2026 Change-Id: I453f94d1b9995deb1e8275a878bf81a22960c08e --- manual/getting_started/installation.tex | 22 +++++++++++----------- manual/intro/main.tex | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/manual/getting_started/installation.tex b/manual/getting_started/installation.tex index 5358c101db..5be1dd92d3 100644 --- a/manual/getting_started/installation.tex +++ b/manual/getting_started/installation.tex @@ -160,7 +160,7 @@ of before installing. \item[Firewire connection.] } To transfer Rockbox to your \dap{} you need to - connect it to your computer.\nopt{rgnano}{For manual installation/uninstallation, or + connect it to your computer. \nopt{rgnano}{For manual installation/uninstallation, or should autodetection fail during automatic installation, you need to know where to access the \dap{}.} On Windows this means you need to know the drive letter associated with the \dap{}. On Linux you need to know @@ -274,26 +274,26 @@ of before installing. } \opt{disk_storage}{ -Rockbox supports very large drive capacities when used with GPT partitioning on devices that utilize ATA storage, but due to the limitations of the FAT32 filesystem, individual paritions cannot exceed 2TiB. Additionally, there may be underlying platform limitations that make it difficult, if not impossible, utilize a drive larger than some arbitrary size or boot from a drive using GPT partitioning. +Rockbox supports very large drive capacities when used with GPT partitioning on devices that utilize ATA storage, but due to the limitations of the FAT32 filesystem, individual paritions cannot exceed 2\,TiB. Additionally, there may be underlying platform limitations that make it difficult, if not impossible, utilize a drive larger than some arbitrary size or boot from a drive using GPT partitioning. \opt{ipod6g}{ -For example, the stock Apple firmware on earlier 6th generation iPod Classic models\footnote{MB029, MB145, MB147, MB150} is limited to drives no larger than 128GiB, and booting into the Apple firmware firmware with larger drive sizes will result in massive data corruption. Later 6th generation iPod Classic models\footnote{MB562, MB565, MC293, MC297} (sometimes referred to as ``7th gen'') do not have this issue. Rockbox itself will function correctly on all 6th/7th generation iPod Classic models. +For example, the stock Apple firmware on earlier 6th generation iPod Classic models\footnote{MB029, MB145, MB147, MB150} is limited to drives no larger than 128\,GiB, and booting into the Apple firmware firmware with larger drive sizes will result in massive data corruption. Later 6th generation iPod Classic models\footnote{MB562, MB565, MC293, MC297} (sometimes referred to as ``7th gen'') do not have this issue. Rockbox itself will function correctly on all 6th/7th generation iPod Classic models. } -\note{In practice, no Rockbox-capable device can currently handle total drive capacities exceeding 2TiB.} +\note{In practice, no Rockbox-capable device can currently handle total drive capacities exceeding 2\,TiB.} } \opt{sd_storage}{ - Rockbox does not currently support SDUC cards, ie those with capacities - exceeding 2TiB. However, this is purely a software limitation with + Rockbox does not currently support SDUC cards, i.e. those with capacities + exceeding 2\,TiB. However, this is purely a software limitation with Rockbox's SD card drivers, and will be addressed in a future release. - \note{SD cards exceeding 32GiB are pre-formated using the exFAT filesystem with GPT paritioning. + \note{SD cards exceeding 32\,GiB are pre-formated using the exFAT filesystem with GPT partitioning. Before they can be used with Rockbox, they must be reformatted with FAT32.} } \opt{disk_storage,sd_storage}{ -\note{The disk format utility included with all versions Microsoft Windows released to date will refuse to create/format FAT32 partitons exceeding 32GiB, so users of larger storage devices will need an alternative tool. Options include GParted Live\footnote{\url{https://gparted.org/}}\opt{ipod}{ or performing a restore using iTunes on Windows}. \opt{sd_storage}{Addtionally, devices that support removable storage typically have a built-in format tool that can be used.}} +\note{The disk format utility included with all versions Microsoft Windows released to date will refuse to create/format FAT32 partitons exceeding 32\,GiB, so users of larger storage devices will need an alternative tool. Options include GParted Live\footnote{\url{https://gparted.org/}}\opt{ipod}{ or performing a restore using iTunes on Windows}. \opt{sd_storage}{Additionally, devices that support removable storage typically have a built-in format tool that can be used.}} } \opt{disk_storage}{ @@ -307,16 +307,16 @@ For example, the stock Apple firmware on earlier 6th generation iPod Classic mod advanced power management and the \emph{removeable} attribute. They are performant, reliable, and physically robust, but tend to be expensive and not available in larger sizes. \item[SATA.] These are fast, reliable, and available in high capacities, but are typically optimized for high performance - at the expense of power consumption.. However, as they implement the full ATA command set, we are able + at the expense of power consumption. However, as they implement the full ATA command set, we are able to minimize their power consumption and power them down when not being actively used. \item[Single Secure Digital (SD).] While these adapters come in different form factors from multiple vendors, they are all based on the same basic design. The ATA command set is incompletely emulated, notably lacking support for \emph{mandatory} ATA power management commands that Rockbox uses to safely transition the device in and out of low power states. Additionally, SD cards vary widely in quality and power consumption with the resultant effects on data longevity and battery life. Finally, these SD adapters - do not support 2TiB or larger SDUC cards. + do not support 2\,TiB or larger SDUC cards. \item[Dual/Quad SD.] These are similar to the above, only allowing use of mulitiple SD cards to - increase the overall storage capacity. While typically described as JBOD\footnote{Just a Bunch Of Disks}, this is not accurate as each card is not individually accessable. Instead, the adapter claims to be to be a single logical drive of the combined capacity of the individual cards in a RAID0-like manner. Consquently, if any one card fails, all data on all other cards may be rendered inacessible. Given the quality concerns mentioned earlier, this means use of multiple SD cards in one of these adapters is the least reliable/robust of the various SSD mods. Finally, in another violation of the ATA specification, these ATA-SD adapters fail to properly support LBA48 addressing, meaning that no matter what combination of cards is used, if their combined capacity exceeds 2TiB, the extra capacity will not be usable, and the device may even present as having (considerably) less space. + increase the overall storage capacity. While typically described as JBOD\footnote{Just a Bunch Of Disks}, this is not accurate as each card is not individually accessable. Instead, the adapter claims to be a single logical drive of the combined capacity of the individual cards in a RAID0-like manner. Consquently, if any one card fails, all data on all other cards may be rendered inacessible. Given the quality concerns mentioned earlier, this means use of multiple SD cards in one of these adapters is the least reliable/robust of the various SSD mods. Finally, in another violation of the ATA specification, these ATA-SD adapters fail to properly support LBA48 addressing, meaning that no matter what combination of cards is used, if their combined capacity exceeds 2\,TiB, the extra capacity will not be usable, and the device may even present as having (considerably) less space. \end{description} \note{All of these flash/SSD mods take up less physical space in the device enclosure than the original hard drive, so care must be taken to ensure they are securely mounted and resistant to the vibration and impacts that typically occur in portable devices. Ribbon cables are particularly vulnerable.} diff --git a/manual/intro/main.tex b/manual/intro/main.tex index 5f73076fec..ec3f16e4e1 100644 --- a/manual/intro/main.tex +++ b/manual/intro/main.tex @@ -39,7 +39,7 @@ Rockbox and this manual is the collaborative effort of the Rockbox team and its contributors. See the appendix for a complete list of contributors. -\copyright~2003--2025 The Rockbox Team and its contributors, +\copyright~2003--2026 The Rockbox Team and its contributors, \copyright~2004 Christi Alice Scarborough, \copyright~2003 Jos\'{e} Maria Garcia-Valdecasas Bernal \& Peter Schlenker. \end{quote} From 420537c8643cc6ffc844115d2fca9e6129f7ce71 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 30 Aug 2026 21:17:29 -0400 Subject: [PATCH 132/408] manual: correct Ipod/Itunes -> iPod/iTunes Change-Id: I54b323085140f0e4abddab7a522e34e897d9edf4 --- manual/getting_started/installation.tex | 48 ++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/manual/getting_started/installation.tex b/manual/getting_started/installation.tex index 5be1dd92d3..a11ed99cef 100644 --- a/manual/getting_started/installation.tex +++ b/manual/getting_started/installation.tex @@ -16,14 +16,14 @@ follow the instructions at \opt{ipodnano,ipodnano2g}{ The \playertype{} is available in multiple versions, not all of which run Rockbox. Rockbox presently runs only on - the first and second generation Ipod Nano. Rockbox does - \emph{not} run on the third, fourth or fifth generation Ipod Nano. - For information on identifying which Ipod you own, see this page on + the first and second generation iPod Nano. Rockbox does + \emph{not} run on the third, fourth or fifth generation iPod Nano. + For information on identifying which iPod you own, see this page on Apple's website: \url{http://www.info.apple.com/kbnum/n61688}. } \opt{ipodvideo}{ The \playertype{} is the 5th/5.5th generation \playerman{} only. - For information on identifying which Ipod you own, see this page on Apple's + For information on identifying which iPod you own, see this page on Apple's website: \url{http://www.info.apple.com/kbnum/n61688}. } \opt{ipod6g}{ @@ -168,10 +168,10 @@ of before installing. name of your \dap{}. \opt{ipod}{ - If you have Itunes installed and it is configured to open automatically + If you have iTunes installed and it is configured to open automatically when your \dap{} is attached (the default behaviour), then wait for it to open and then quit it. You also need to ensure the ``Enable use as disk'' - option is enabled for your \dap{} in Itunes. Your \dap{} should then enter + option is enabled for your \dap{} in iTunes. Your \dap{} should then enter disk mode automatically when connected to a computer via \nopt{ipod1g2g}{USB.}\opt{ipod1g2g}{Firewire.} If your computer does not recognise your \dap{}, you may need to enter disk mode manually. Disconnect @@ -258,14 +258,14 @@ of before installing. \opt{ipod}{ \begin{description} - \item[File system format.] Rockbox only works on Ipods formatted with - the FAT32 filesystem (i.e. Ipods initialised by Itunes - for Windows). It does not work with the HFS+ filesystem (i.e. Ipods - initialised by Itunes for the Mac). More information and instructions for - converting an Ipod to FAT32 can be found on the + \item[File system format.] Rockbox only works on iPods formatted with + the FAT32 filesystem (i.e. iPods initialised by iTunes + for Windows). It does not work with the HFS+ filesystem (i.e. iPods + initialised by iTunes for the Mac). More information and instructions for + converting an iPod to FAT32 can be found on the \wikilink{IpodConversionToFAT32} wiki page on the Rockbox website. Note that after conversion, you can still use - a FAT32 Ipod with a Mac. + a FAT32 iPod with a Mac. \end{description} } @@ -274,19 +274,19 @@ of before installing. } \opt{disk_storage}{ -Rockbox supports very large drive capacities when used with GPT partitioning on devices that utilize ATA storage, but due to the limitations of the FAT32 filesystem, individual paritions cannot exceed 2\,TiB. Additionally, there may be underlying platform limitations that make it difficult, if not impossible, utilize a drive larger than some arbitrary size or boot from a drive using GPT partitioning. +Rockbox supports very large drive (over 2\,TiB) capacities when used with GPT partitioning on \dap{}s that utilize ATA storage, but due to the limitations of the FAT32 filesystem, individual paritions cannot exceed 2\,TiB. Additionally, there may be underlying platform limitations that make it difficult, if not impossible, utilize a drive larger than some arbitrary size or boot from a drive using GPT partitioning. \opt{ipod6g}{ For example, the stock Apple firmware on earlier 6th generation iPod Classic models\footnote{MB029, MB145, MB147, MB150} is limited to drives no larger than 128\,GiB, and booting into the Apple firmware firmware with larger drive sizes will result in massive data corruption. Later 6th generation iPod Classic models\footnote{MB562, MB565, MC293, MC297} (sometimes referred to as ``7th gen'') do not have this issue. Rockbox itself will function correctly on all 6th/7th generation iPod Classic models. } -\note{In practice, no Rockbox-capable device can currently handle total drive capacities exceeding 2\,TiB.} +\note{In practice, no Rockbox-capable \dap{} can currently handle total drive capacities exceeding 2\,TiB.} } \opt{sd_storage}{ Rockbox does not currently support SDUC cards, i.e. those with capacities exceeding 2\,TiB. However, this is purely a software limitation with - Rockbox's SD card drivers, and will be addressed in a future release. + Rockbox's SD controller drivers, and will be addressed in a future release. \note{SD cards exceeding 32\,GiB are pre-formated using the exFAT filesystem with GPT partitioning. Before they can be used with Rockbox, they must be reformatted with FAT32.} @@ -311,17 +311,17 @@ For example, the stock Apple firmware on earlier 6th generation iPod Classic mod to minimize their power consumption and power them down when not being actively used. \item[Single Secure Digital (SD).] While these adapters come in different form factors from multiple vendors, they are all based on the same basic design. The ATA command set is incompletely emulated, notably lacking - support for \emph{mandatory} ATA power management commands that Rockbox uses to safely + support for several \emph{mandatory} ATA power management commands that Rockbox uses to safely transition the device in and out of low power states. Additionally, SD cards vary widely in quality - and power consumption with the resultant effects on data longevity and battery life. Finally, these SD adapters - do not support 2\,TiB or larger SDUC cards. + and power consumption with the resultant effects on data longevity and battery life. Finally, to date, none + of these SD adapters support 2\,TiB or larger SDUC cards. \item[Dual/Quad SD.] These are similar to the above, only allowing use of mulitiple SD cards to increase the overall storage capacity. While typically described as JBOD\footnote{Just a Bunch Of Disks}, this is not accurate as each card is not individually accessable. Instead, the adapter claims to be a single logical drive of the combined capacity of the individual cards in a RAID0-like manner. Consquently, if any one card fails, all data on all other cards may be rendered inacessible. Given the quality concerns mentioned earlier, this means use of multiple SD cards in one of these adapters is the least reliable/robust of the various SSD mods. Finally, in another violation of the ATA specification, these ATA-SD adapters fail to properly support LBA48 addressing, meaning that no matter what combination of cards is used, if their combined capacity exceeds 2\,TiB, the extra capacity will not be usable, and the device may even present as having (considerably) less space. \end{description} \note{All of these flash/SSD mods take up less physical space in the device enclosure than the original hard drive, so care must be taken to ensure they are securely mounted and resistant to the vibration and impacts that typically occur in portable devices. Ribbon cables are particularly vulnerable.} -It is also worth noting that you will not likely see improved data transfer rates when using solid-state storage; the underlying \dap{} hardware is simply too slow to materially benefit. However, the improved access times should help the \dap{} to feel much more responsive. +It is also worth noting that data transfer rates are not likely to be improved when using solid-state storage; the underlying \dap{} hardware is simply too slow to materially benefit. However, the improved access times should help the \dap{} to feel much more responsive. } \section{Installing Rockbox}\label{sec:installing_rockbox}\index{Installation} @@ -809,7 +809,7 @@ to speak file and folder names. \section{Running Rockbox} \nopt{ipod,gigabeats,cowond2,rgnano}{When you turn the unit on, Rockbox should load.} -\opt{ipod}{Hard reset the Ipod by holding +\opt{ipod}{Hard reset the iPod by holding \opt{IPOD_4G_PAD}{\ButtonMenu{} and \ButtonSelect{} simultaneously}% \opt{IPOD_3G_PAD}{\ButtonMenu{} and \ButtonPlay{} simultaneously} for a couple of seconds until the \dap{} resets. Now Rockbox should load. @@ -837,11 +837,11 @@ you turn the unit on, Rockbox should load.} \opt{ipod}{ \note{ - If you have loaded music onto your \dap{} using Itunes, + If you have loaded music onto your \dap{} using iTunes, you will not be able to see your music properly in the \setting{File Browser}. - This is because Itunes changes your files' names and hides them in + This is because iTunes changes your files' names and hides them in directories in the \fname{Ipod\_Control} directory. Files placed on your - \dap{} using Itunes can be viewed by initialising and using Rockbox's database. + \dap{} using iTunes can be viewed by initialising and using Rockbox's database. See \reference{ref:database} for more information. } } @@ -934,7 +934,7 @@ completely follow the manual uninstallation instructions below.}} } \optv{ipod}{ - To uninstall Rockbox and go back to using just the original Ipod software, connect + To uninstall Rockbox and go back to using just the original iPod software, connect the \dap{} to your computer and follow the instructions to install the bootloader but, when prompted by ipodpatcher, enter \texttt{u} for uninstall instead of \texttt{i} for install. From 3664373ce731488181322b7e2ab341dce0659f4d Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Mon, 31 Aug 2026 22:39:44 -0400 Subject: [PATCH 133/408] =?UTF-8?q?FS#13992=20-=20Updated=20Vietnamese=20T?= =?UTF-8?q?ranslation=20(Chu=20Kh=C3=A1nh=20H=E1=BA=A1nh)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Ic0567f972cebbe152ba9b9badf6d60c17b0e6ddc --- apps/lang/vietnamese.lang | 202 +++++++++++++++++++++++++++++++++----- 1 file changed, 178 insertions(+), 24 deletions(-) diff --git a/apps/lang/vietnamese.lang b/apps/lang/vietnamese.lang index ea8190f9f5..2bc418cdcc 100644 --- a/apps/lang/vietnamese.lang +++ b/apps/lang/vietnamese.lang @@ -1006,10 +1006,10 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "~Peak Filter %d" + *: "~Peak Filter" *: "~Peak filter" @@ -14689,7 +14689,7 @@ user: core *: "Press LEFT to cancel." - android,hifietma*,zenvision: "Press BACK to cancel." + android,hifietma*: "Press BACK to cancel." cowond2,creativezenxfi2,ibassodx50,ibassodx90,mrobe500,ondavx747: "Press POWER to cancel." ihifi760,ihifi960: "Double tap RETURN to cancel." ihifi770,ihifi770c,ihifi800: "Press HOME to cancel." @@ -14704,7 +14704,7 @@ *: "Nhấn LEFT để hủy." - android,hifietma*,zenvision: "Nhấn BACK để hủy." + android,hifietma*: "Nhấn BACK để hủy." cowond2,creativezenxfi2,ibassodx50,ibassodx90,mrobe500,ondavx747: "Nhấn POWER để hủy." ihifi760,ihifi960: "Nhấn đúp RETURN để hủy." ihifi770,ihifi770c,ihifi800: "Nhấn HOME để hủy." @@ -14719,7 +14719,7 @@ *: "Nhấn LEFT để hủy." - android,hifietma*,zenvision: "Nhấn BACK để hủy." + android,hifietma*: "Nhấn BACK để hủy." cowond2,creativezenxfi2,ibassodx50,ibassodx90,mrobe500,ondavx747: "Nhấn POWER để hủy." ihifi760,ihifi960: "Nhấn đúp RETURN để hủy." ihifi770,ihifi770c,ihifi800: "Nhấn HOME để hủy." @@ -14960,20 +14960,6 @@ *: "Không phải là plugin" - - id: LANG_OPEN_PLUGIN_SET_WPS_CONTEXT_PLUGIN - desc: open plugin module - user: core - - *: "Set WPS Context Plugin" - - - *: "Đặt ngữ cảnh Wps của Plugin" - - - *: "Đặt ngữ cảnh WPS của Plugin" - - id: LANG_PARAMETER desc: @@ -15214,7 +15200,7 @@ id: LANG_VOICED_DATE_FORMAT - desc: format string for how dates will be read back. Y == 4-digit year, A == month name, m == numeric month, d == numeric day. For example, "AdY" will read "January 21 2021" + desc: format string for how dates will be read back. Y == 4-digit year (grouped), y == 4-digit year (numeric), A == month name, m == numeric month, d == numeric day. For example, for 2021-01-05, "AdY" will be voiced as "January 5 twenty twenty-one" and "dmy" will be voiced as "5 1 two thousand twenty one user: core *: "dAY" @@ -15823,16 +15809,16 @@ id: LANG_DEFAULT_BROWSER - desc: in Settings + desc: deprecated user: core - *: "Default Browser" + *: "" - *: "Trình duyệt mặc định" + *: "" - *: "Trình duyệt mặc định" + *: "" @@ -16971,3 +16957,171 @@ *: "~U S B" + + id: LANG_SET_CONTEXT_ITEM + desc: WPS context item + user: core + + *: "Set Context Item" + + + *: "Đặt Vật phẩm Bối cảnh" + + + *: "Đặt Vật phẩm Bối cảnh" + + + + id: LANG_KEEP_DIRECTORY + desc: file browser setting + user: core + + *: "Always remember last folder" + + + *: "Luôn ghi nhớ thư mục lần trước" + + + *: "Luôn ghi nhớ thư mục lần trước" + + + + id: LANG_FILE_NOT_FOUND + desc: When file does not exist + user: core + + *: "File not found" + + + *: "Không tìm thấy tập tin" + + + *: "Không tìm thấy tập tin" + + + + id: LANG_SHOW_IN_FILES + desc: Reveal item in File Browser + user: core + + *: "Show in Files" + + + *: "Hiển thị trong Tập Tin" + + + *: "Hiển thị trong Tập Tin" + + + + id: LANG_CHANNEL_SWAP + desc: in sound_settings + user: core + + *: "Swap Left & Right" + + + *: "Hoán đổi Trái & Phảii" + + + *: "Hoán đổi Trái & Phảii" + + + + id: LANG_COUNTDOWN_TIMER_SET + desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration + user: core + + *: "SET TIMER" + + + *: "ĐẶT KHOẢNG THỜI GIAN" + + + *: "Đặt khoảng thời gian" + + + + id: LANG_COUNTDOWN_TIMER_RUNNING + desc: countdown_timer plugin - status label shown while the countdown is active + user: core + + *: "RUNNING" + + + *: "ĐANG CHẠY" + + + *: "Đang chạy" + + + + id: LANG_COUNTDOWN_TIMER_PAUSED + desc: countdown_timer plugin - status label shown while the countdown is paused + user: core + + *: "PAUSED" + + + *: "Tạm dừng" + + + *: "Tạm dừng" + + + + id: LANG_COUNTDOWN_TIMER_OVERTIME + desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up + user: core + + *: "OVERTIME" + + + *: "QUÁ GIỜ" + + + *: "Quá giờ" + + + + id: LANG_COUNTDOWN_TIMER_FINISHED + desc: countdown_timer plugin - status label shown at the moment the countdown expires + user: core + + *: "FINISHED" + + + *: "HOÀN THÀNH" + + + *: "Hoàn thành" + + + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Bộ lọc %d" + + + *: "Bộ lọc" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Kiểu Bộ lọc" + + + *: "Kiểu bộ lọc" + + From 3e996ec73e7013f11512c28af8c8346f26b7e121 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 1 Sep 2026 07:36:48 -0400 Subject: [PATCH 134/408] FS#13993 - rbutl push decompression onto a separate thread (Alessio Lenzi) During Rockbox installation or update, the progress window currently becomes unresponsive to screen-reader navigation while archive inspection, free-space calculation and extraction are in progress. A blind user cannot reliably move through or read the status messages and therefore cannot determine what the utility is doing, how far the installation has progressed, or whether an error has occurred. The cause is that these operations are performed synchronously in the GUI thread after a package has been downloaded. Although the installation itself continues, the user interface cannot process keyboard input and accessibility events reliably until the operation finishes. The attached patch moves the package installation work to a low-priority QThread. Downloading remains asynchronous as before, while progress and log signals from archive extraction are delivered back to the GUI thread through Qt connections. This keeps the progress window fully navigable with a screen reader throughout the installation. Change-Id: I9a36a736e5b4bf98de8c3a71151679a4c1333dc7 --- utils/rbutilqt/base/zipinstaller.cpp | 187 ++++++++++++++++++++------- utils/rbutilqt/base/zipinstaller.h | 5 + 2 files changed, 147 insertions(+), 45 deletions(-) diff --git a/utils/rbutilqt/base/zipinstaller.cpp b/utils/rbutilqt/base/zipinstaller.cpp index f97adfb492..77caf7b937 100644 --- a/utils/rbutilqt/base/zipinstaller.cpp +++ b/utils/rbutilqt/base/zipinstaller.cpp @@ -23,13 +23,118 @@ #include "ziputil.h" #include "Logger.h" +class ZipInstallThread : public QThread +{ +public: + enum Result { + Success, + OpenFailed, + NotEnoughSpace, + ExtractionFailed, + CopyFailed + }; + + explicit ZipInstallThread(ZipInstaller *owner) + : QThread(owner), m_owner(owner), m_unzip(true), + m_result(OpenFailed), m_requiredSpace(0) + { + } + + void setArchive(const QString& file, const QString& mountpoint) + { + m_file = file; + m_mountpoint = mountpoint; + m_unzip = true; + } + + void setCopy(const QString& file, const QString& mountpoint, + const QString& target) + { + m_file = file; + m_mountpoint = mountpoint; + m_target = target; + m_unzip = false; + } + + Result result(void) const { return m_result; } + QStringList installedFiles(void) const { return m_installedFiles; } +protected: + void run(void) override + { + if(m_unzip) { + ZipUtil zip(nullptr); + connect(&zip, &ZipUtil::logProgress, + m_owner, &ZipInstaller::logProgress); + connect(&zip, &ZipUtil::logItem, + m_owner, &ZipInstaller::logItem); + + if(!zip.open(m_file, QuaZip::mdUnzip)) { + m_result = OpenFailed; + return; + } + + m_requiredSpace = zip.totalUncompressedSize( + Utils::filesystemClusterSize(m_mountpoint)); + if((qint64)Utils::filesystemFree(m_mountpoint) + < m_requiredSpace + 1000000) { + zip.close(); + m_result = NotEnoughSpace; + return; + } + + m_installedFiles = zip.files(); + if(!zip.extractArchive(m_mountpoint)) { + m_result = ExtractionFailed; + zip.close(); + return; + } + zip.close(); + } + else { + QString destfile = m_mountpoint + "/" + m_target; + QString path = QFileInfo(destfile).absolutePath(); + if(!QDir().mkpath(path)) { + m_result = CopyFailed; + return; + } + QFile(destfile).remove(); + if(!QFile::copy(m_file, destfile)) { + m_result = CopyFailed; + return; + } + m_installedFiles.append(m_target); + } + + m_result = Success; + } + +private: + ZipInstaller *m_owner; + QString m_file; + QString m_mountpoint; + QString m_target; + QStringList m_installedFiles; + bool m_unzip; + Result m_result; + qint64 m_requiredSpace; +}; + ZipInstaller::ZipInstaller(QObject* parent) : QObject(parent), - m_unzip(true), m_usecache(false), m_getter(nullptr) + m_unzip(true), m_usecache(false), m_getter(nullptr), + m_installThread(nullptr) { } +ZipInstaller::~ZipInstaller() +{ + if(m_installThread && m_installThread->isRunning()) { + m_installThread->wait(); + } +} + + void ZipInstaller::install() { LOG_INFO() << "initializing installation"; @@ -106,7 +211,6 @@ void ZipInstaller::installStart() void ZipInstaller::downloadDone(QNetworkReply::NetworkError error) { LOG_INFO() << "download done, error:" << error; - QStringList zipContents; // needed later // update progress bar emit logProgress(1, 1); @@ -128,64 +232,57 @@ void ZipInstaller::downloadDone(QNetworkReply::NetworkError error) else { emit logItem(tr("Download finished."),LOGOK); } - QCoreApplication::processEvents(); if(m_unzip) { - // unzip downloaded file LOG_INFO() << "about to unzip" << m_file << "to" << m_mountpoint; - emit logItem(tr("Extracting file."), LOGINFO); - QCoreApplication::processEvents(); - - ZipUtil zip(this); - connect(&zip, &ZipUtil::logProgress, this, &ZipInstaller::logProgress); - connect(&zip, &ZipUtil::logItem, this, &ZipInstaller::logItem); - zip.open(m_file, QuaZip::mdUnzip); - // check for free space. Make sure after installation will still be - // some room for operating (also includes calculation mistakes due to - // cluster sizes on the player). - if((qint64)Utils::filesystemFree(m_mountpoint) - < (zip.totalUncompressedSize( - Utils::filesystemClusterSize(m_mountpoint)) - + 1000000)) { - emit logItem(tr("Not enough disk space! Aborting."), LOGERROR); - emit logProgress(1, 1); - emit done(true); - return; - } - zipContents = zip.files(); - if(!zip.extractArchive(m_mountpoint)) { - emit logItem(tr("Extraction failed!"), LOGERROR); - emit logProgress(1, 1); - emit done(true); - return; - } - zip.close(); + m_installThread = new ZipInstallThread(this); + m_installThread->setArchive(m_file, m_mountpoint); } else { if (m_target.isEmpty()) m_target = QUrl(m_url).fileName(); QString destfile = m_mountpoint + "/" + m_target; - // only copy the downloaded file to the output location / name emit logItem(tr("Installing file."), LOGINFO); LOG_INFO() << "saving downloaded file (no extraction) to" << destfile; + // Keep the temporary file materialized while the worker copies it. + m_downloadFile->open(); + m_installThread = new ZipInstallThread(this); + m_installThread->setCopy(m_file, m_mountpoint, m_target); + } - m_downloadFile->open(); // copy fails if file is not opened (filename issue?) - // make sure the required path is existing - QString path = QFileInfo(destfile).absolutePath(); - QDir p; - p.mkpath(path); - // QFile::copy() doesn't overwrite files, so remove old one first - // TODO: compare old and new file and fail if those are different. - QFile(destfile).remove(); - if(!m_downloadFile->copy(destfile)) { + connect(m_installThread, &QThread::finished, + this, &ZipInstaller::installFinished); + m_installThread->start(QThread::LowPriority); +} + + +void ZipInstaller::installFinished() +{ + ZipInstallThread *thread = m_installThread; + m_installThread = nullptr; + ZipInstallThread::Result result = thread->result(); + QStringList zipContents = thread->installedFiles(); + thread->deleteLater(); + + emit logProgress(1, 1); + switch(result) { + case ZipInstallThread::Success: + break; + case ZipInstallThread::NotEnoughSpace: + emit logItem(tr("Not enough disk space! Aborting."), LOGERROR); + emit done(true); + return; + case ZipInstallThread::CopyFailed: emit logItem(tr("Installing file failed."), LOGERROR); emit done(true); return; - } - - // add file to log - zipContents.append(m_target); + case ZipInstallThread::OpenFailed: + case ZipInstallThread::ExtractionFailed: + emit logItem(tr("Extraction failed!"), LOGERROR); + emit done(true); + return; } + if(m_logver.isEmpty()) { // if no version info is set use the timestamp of the server file. m_logver = m_getter->timestamp().toString(Qt::ISODate); diff --git a/utils/rbutilqt/base/zipinstaller.h b/utils/rbutilqt/base/zipinstaller.h index 87c38b7700..462ccfd988 100644 --- a/utils/rbutilqt/base/zipinstaller.h +++ b/utils/rbutilqt/base/zipinstaller.h @@ -28,6 +28,8 @@ #include "httpget.h" #include "Logger.h" +class ZipInstallThread; + /** Install a file or zip. * Downloads file(s) from a given URL, and installs by either extracting or * copying it to the target path set by setMountpoint(). @@ -37,6 +39,7 @@ class ZipInstaller : public QObject Q_OBJECT public: ZipInstaller(QObject* parent); + ~ZipInstaller(); void install(void); void setMountPoint(QString& mountpoint) {m_mountpoint = mountpoint;} void setUrl(QString& url){m_urllist = QStringList(url);} @@ -59,6 +62,7 @@ public slots: private slots: void downloadDone(QNetworkReply::NetworkError error); + void installFinished(void); void installStart(void); void installContinue(void); @@ -79,6 +83,7 @@ private: HttpGet *m_getter; QTemporaryFile *m_downloadFile; + ZipInstallThread *m_installThread; }; From 8e965d9159b43c7ccd500d2b45ccff83c9de055a Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 1 Sep 2026 07:39:10 -0400 Subject: [PATCH 135/408] FS#13994 - Updated Dutch Translation (Marc Aarts) Change-Id: I3e6f3062ae9e5ddc63b65e2a23ad984f13b0ba72 --- apps/lang/nederlands.lang | 216 +++++++++++++++++++++++++++++++++----- 1 file changed, 192 insertions(+), 24 deletions(-) diff --git a/apps/lang/nederlands.lang b/apps/lang/nederlands.lang index aea94e33ae..398e62d26f 100644 --- a/apps/lang/nederlands.lang +++ b/apps/lang/nederlands.lang @@ -737,10 +737,10 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Piekfilter %d" + *: "Peak Filter" *: "Piekfilter" @@ -14434,7 +14434,7 @@ user: core *: "Press LEFT to cancel." - android,hifietma*,zenvision: "Press BACK to cancel." + android,hifietma*: "Press BACK to cancel." cowond2,creativezenxfi2,ibassodx50,ibassodx90,mrobe500,ondavx747: "Press POWER to cancel." ihifi760,ihifi960: "Double tap RETURN to cancel." ihifi770,ihifi770c,ihifi800: "Press HOME to cancel." @@ -14450,7 +14450,7 @@ *: "Druk op LINKS om te annuleren." android: "Druk op RECHTS om te annuleren." - hifietma*,zenvision: "Druk op BACK om te annuleren." + hifietma*: "Druk op BACK om te annuleren." cowond2,creativezenxfi2,ibassodx50,ibassodx90,mrobe500,ondavx747: "Druk op POWER om te annuleren." ihifi760,ihifi960: "Dubbeltik op RETURN om te annuleren." ihifi770,ihifi770c,ihifi800: "Druk op HOME om te annuleren." @@ -14466,7 +14466,7 @@ *: "Druk op LINKS om te annuleren." android: "Druk op RECHTS om te annuleren." - hifietma*,zenvision: "Druk op BACK om te annuleren." + hifietma*: "Druk op BACK om te annuleren." cowond2,creativezenxfi2,ibassodx50,ibassodx90,mrobe500,ondavx747: "Druk op POWER om te annuleren." ihifi760,ihifi960: "Dubbeltik op RETURN om te annuleren." ihifi770,ihifi770c,ihifi800: "Druk op HOME om te annuleren." @@ -14707,20 +14707,6 @@ *: "Geen plug-in" - - id: LANG_OPEN_PLUGIN_SET_WPS_CONTEXT_PLUGIN - desc: open plugin module - user: core - - *: "Set WPS Context Plugin" - - - *: "Stel WPS Context Plugin in" - - - *: "Stel WPS Context Plugin in" - - id: LANG_PARAMETER desc: @@ -15202,7 +15188,7 @@ id: LANG_VOICED_DATE_FORMAT - desc: format string for how dates will be read back. Y == 4-digit year, A == month name, m == numeric month, d == numeric day. For example, "AdY" will read "January 21 2021" + desc: format string for how dates will be read back. Y == 4-digit year (grouped), y == 4-digit year (numeric), A == month name, m == numeric month, d == numeric day. For example, for 2021-01-05, "AdY" will be voiced as "January 5 twenty twenty-one" and "dmy" will be voiced as "5 1 two thousand twenty one user: core *: "dAY" @@ -15811,16 +15797,16 @@ id: LANG_DEFAULT_BROWSER - desc: in Settings + desc: deprecated user: core - *: "Default Browser" + *: "" - *: "Standaard Browser" + *: "" - *: "Standaard Browser" + *: "" @@ -16967,3 +16953,185 @@ *: "~U S B" + + id: LANG_ANNOUNCE_STATUS + desc: announnnce_status plugin + user: core + + *: "Announce Status" + + + *: "Status aankondigen" + + + *: "Status aankondigen" + + + + id: LANG_SET_CONTEXT_ITEM + desc: WPS context item + user: core + + *: "Set Context Item" + + + *: "Contextitem instellen" + + + *: "Contextitem instellen" + + + + id: LANG_KEEP_DIRECTORY + desc: file browser setting + user: core + + *: "Always remember last folder" + + + *: "Altijd laatste map onthouden" + + + *: "Altijd laatste map onthouden" + + + + id: LANG_FILE_NOT_FOUND + desc: When file does not exist + user: core + + *: "File not found" + + + *: "Bestand niet gevonden" + + + *: "Bestand niet gevonden" + + + + id: LANG_SHOW_IN_FILES + desc: Reveal item in File Browser + user: core + + *: "Show in Files" + + + *: "In bestanden tonen" + + + *: "In bestanden tonen" + + + + id: LANG_CHANNEL_SWAP + desc: in sound_settings + user: core + + *: "Swap Left & Right" + + + *: "Links & Rechts omwisselen" + + + *: "Links & Rechts omwisselen" + + + + id: LANG_COUNTDOWN_TIMER_SET + desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration + user: core + + *: "SET TIMER" + + + *: "TIMER INSTELLEN" + + + *: "Timer instellen" + + + + id: LANG_COUNTDOWN_TIMER_RUNNING + desc: countdown_timer plugin - status label shown while the countdown is active + user: core + + *: "RUNNING" + + + *: "ACTIEF" + + + *: "Actief" + + + + id: LANG_COUNTDOWN_TIMER_PAUSED + desc: countdown_timer plugin - status label shown while the countdown is paused + user: core + + *: "PAUSED" + + + *: "GEPAUZEERD" + + + *: "Gepauzeerd" + + + + id: LANG_COUNTDOWN_TIMER_OVERTIME + desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up + user: core + + *: "OVERTIME" + + + *: "OVER TIJD" + + + *: "Over tijd" + + + + id: LANG_COUNTDOWN_TIMER_FINISHED + desc: countdown_timer plugin - status label shown at the moment the countdown expires + user: core + + *: "FINISHED" + + + *: "AFGELOPEN" + + + *: "Afgelopen" + + + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Filter %d" + + + *: "Filter" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Filter Type" + + + *: "Filter Type" + + From e498c0171a21c1472f5692ffd4db75e5f9ba23ed Mon Sep 17 00:00:00 2001 From: Mauricio Garrido Date: Mon, 31 Aug 2026 18:02:32 -0600 Subject: [PATCH 136/408] 3ds: Port refactor. This commit does the following changes to the 3ds port: - Rename the target from ctru to 3ds. - Rename all files and functions with the ctru naming convention to 3ds. - Created a new file and folder structure that will better integrate future console ports that share the same codebase. - Fixed a buffer overflow bug in pcm code. Change-Id: I17c6f86df64eb99dd2b653485d70832ff46b2ba8 --- apps/SOURCES | 4 +- apps/keymaps/{keymap-ctru.c => keymap-3ds.c} | 2 +- apps/plugins/SUBDIRS | 2 +- apps/plugins/battery_bench.c | 2 +- apps/plugins/blackjack.c | 2 +- apps/plugins/brickmania.c | 2 +- apps/plugins/calculator.c | 2 +- apps/plugins/calendar.c | 2 +- apps/plugins/chessbox/chessbox_pgn.h | 2 +- apps/plugins/chessclock.c | 2 +- apps/plugins/chip8.c | 2 +- apps/plugins/chopper.c | 2 +- apps/plugins/clix.c | 2 +- apps/plugins/cube.c | 2 +- apps/plugins/doom/i_video.c | 2 +- apps/plugins/flipit.c | 2 +- apps/plugins/fractals/fractal.h | 2 +- apps/plugins/goban/goban.h | 2 +- apps/plugins/imageviewer/imageviewer_button.h | 2 +- apps/plugins/invadrox.c | 2 +- apps/plugins/jewels.c | 2 +- apps/plugins/lib/keymaps.h | 4 +- apps/plugins/lib/pluginlib_actions.c | 4 +- apps/plugins/midi/midiplay.c | 2 +- apps/plugins/mikmod/mikmod.c | 2 +- apps/plugins/minesweeper.c | 2 +- apps/plugins/mp3_encoder.c | 2 +- apps/plugins/mpegplayer/mpeg_settings.c | 2 +- apps/plugins/mpegplayer/mpegplayer.c | 2 +- apps/plugins/oscilloscope.c | 2 +- apps/plugins/pacbox/pacbox.h | 2 +- apps/plugins/pdbox/pdbox.h | 2 +- apps/plugins/pegbox.c | 2 +- apps/plugins/plugin_crt0.c | 2 +- apps/plugins/pong.c | 2 +- apps/plugins/reversi/reversi-gui.h | 2 +- apps/plugins/rockblox.c | 2 +- apps/plugins/rockboy/rockboy.c | 4 +- apps/plugins/rockpaint.c | 2 +- apps/plugins/sdl/sdl.make | 4 +- apps/plugins/sliding_puzzle.c | 2 +- apps/plugins/snake.c | 2 +- apps/plugins/snake2.c | 2 +- apps/plugins/sokoban.c | 2 +- apps/plugins/solitaire.c | 2 +- apps/plugins/spacerocks.c | 2 +- apps/plugins/star.c | 2 +- apps/plugins/stopwatch.c | 2 +- apps/plugins/sudoku/sudoku.h | 2 +- apps/plugins/text_viewer/tv_button.h | 2 +- apps/plugins/vu_meter.c | 2 +- apps/plugins/wormlet.c | 2 +- apps/plugins/xobox.c | 2 +- apps/plugins/zxbox/keymaps.h | 2 +- apps/plugins/zxbox/zxbox_keyb.c | 2 +- apps/tree.c | 2 +- apps/tree.h | 2 +- firmware/SOURCES | 61 +-- firmware/drivers/audio/{ctru.c => 3ds.c} | 0 firmware/export/audiohw.h | 2 +- firmware/export/config.h | 18 +- firmware/export/config/{ctru.h => 3ds.h} | 8 +- firmware/export/debug.h | 4 +- firmware/export/rbpaths.h | 4 +- firmware/include/dir.h | 4 +- firmware/include/file.h | 4 +- firmware/include/rbendian.h | 2 +- firmware/kernel/include/thread.h | 2 +- firmware/kernel/mutex.c | 2 +- firmware/kernel/thread-common.c | 6 +- firmware/kernel/thread-internal.h | 2 +- firmware/target/hosted/ctru/timer-ctru_sys.c | 60 --- firmware/target/hosted/filesystem-app.h | 4 - ...filesystem-ctru.h => filesystem-console.h} | 70 ++-- .../3ds/backlight-3ds.c} | 2 +- .../3ds/button-3ds.c} | 2 +- .../3ds/filesystem-3ds.c} | 0 .../lc-ctru.c => game_console/3ds/lc-3ds.c} | 11 +- .../3ds}/lc-program-resolver.c | 0 .../3ds/lcd-bitmap-3ds.c} | 2 +- .../3ds/luminance-3ds.c} | 2 +- .../3ds/luminance-3ds.h} | 6 +- .../pcm-ctru.c => game_console/3ds/pcm-3ds.c} | 10 +- .../3ds/powermgmt-3ds.c} | 0 .../3ds/system-3ds.c} | 6 +- .../{ctru/app => game_console}/adc-target.h | 0 .../app => game_console}/backlight-target.h | 0 .../button-console.h} | 6 +- .../app => game_console}/button-target.h | 0 .../hosted/{ctru => game_console}/buttonmap.h | 0 .../kernel-console.c} | 36 +- .../{ctru => game_console}/lcd-bitmap.h | 0 .../{ctru => game_console}/lcd-target.h | 0 .../lib/bfile/bfile-3ds.c} | 53 --- .../lib/bfile/bfile-internal.h | 34 +- .../game_console/lib/bfile/bfile-rwmutex.c | 61 +++ .../{ctru => game_console}/lib/bfile/bfile.h | 0 .../hosted/game_console/lib/sys_atomic.c | 60 +++ .../hosted/game_console/lib/sys_atomic.h | 37 ++ .../game_console/lib/sys_cond_generic.c | 154 +++++++ .../lib/sys_dir_3ds.c} | 40 +- .../{ctru => game_console}/lib/sys_file.h | 19 +- .../lib/sys_file_3ds.c} | 379 +---------------- .../hosted/game_console/lib/sys_file_common.c | 384 ++++++++++++++++++ .../game_console/lib/sys_file_internal.h | 63 +++ .../{ctru => game_console}/lib/sys_thread.h | 62 +-- .../lib/sys_thread_3ds.c} | 328 ++++++--------- .../{ctru => game_console}/lib/sys_timer.h | 0 .../hosted/game_console/lib/sys_timer_3ds.c | 71 ++++ .../lib/sys_timer_common.c} | 98 ++--- .../hosted/game_console/lib/sys_types.h | 40 ++ .../system-console.h} | 0 .../{ctru => game_console}/system-target.h | 2 +- .../thread-console.c} | 144 ++++--- .../thread-console.h} | 6 +- .../timer-console.c} | 0 firmware/target/hosted/rtc.c | 4 +- lib/rbcodec/codecs/codec_crt0.c | 2 +- packaging/{ctru/ctru.make => 3ds/3ds.make} | 10 +- packaging/{ctru => 3ds}/res/app.rsf | 0 packaging/{ctru => 3ds}/res/banner.cgfx | Bin packaging/{ctru => 3ds}/res/banner.png | Bin packaging/{ctru => 3ds}/res/banner.wav | Bin packaging/{ctru => 3ds}/res/banner_.cgfx | Bin packaging/{ctru => 3ds}/res/icon.png | Bin packaging/{ctru => 3ds}/res/icon_yellow.png | Bin packaging/{ctru => 3ds}/rockbox.xml | 0 tools/buildzip.pl | 8 +- tools/configure | 18 +- tools/root.make | 4 +- 130 files changed, 1429 insertions(+), 1112 deletions(-) rename apps/keymaps/{keymap-ctru.c => keymap-3ds.c} (99%) rename firmware/drivers/audio/{ctru.c => 3ds.c} (100%) rename firmware/export/config/{ctru.h => 3ds.h} (94%) delete mode 100644 firmware/target/hosted/ctru/timer-ctru_sys.c rename firmware/target/hosted/{ctru/filesystem-ctru.h => filesystem-console.h} (55%) rename firmware/target/hosted/{ctru/backlight-ctru.c => game_console/3ds/backlight-3ds.c} (99%) rename firmware/target/hosted/{ctru/button-ctru.c => game_console/3ds/button-3ds.c} (99%) rename firmware/target/hosted/{ctru/filesystem-ctru.c => game_console/3ds/filesystem-3ds.c} (100%) rename firmware/target/hosted/{ctru/lc-ctru.c => game_console/3ds/lc-3ds.c} (90%) rename firmware/target/hosted/{ctru => game_console/3ds}/lc-program-resolver.c (100%) rename firmware/target/hosted/{ctru/lcd-bitmap.c => game_console/3ds/lcd-bitmap-3ds.c} (99%) rename firmware/target/hosted/{ctru/luminance-ctru.c => game_console/3ds/luminance-3ds.c} (99%) rename firmware/target/hosted/{ctru/luminance-ctru.h => game_console/3ds/luminance-3ds.h} (93%) rename firmware/target/hosted/{ctru/pcm-ctru.c => game_console/3ds/pcm-3ds.c} (97%) rename firmware/target/hosted/{ctru/powermgmt-ctru.c => game_console/3ds/powermgmt-3ds.c} (100%) rename firmware/target/hosted/{ctru/system-ctru.c => game_console/3ds/system-3ds.c} (97%) rename firmware/target/hosted/{ctru/app => game_console}/adc-target.h (100%) rename firmware/target/hosted/{ctru/app => game_console}/backlight-target.h (100%) rename firmware/target/hosted/{ctru/button-ctru.h => game_console/button-console.h} (92%) rename firmware/target/hosted/{ctru/app => game_console}/button-target.h (100%) rename firmware/target/hosted/{ctru => game_console}/buttonmap.h (100%) rename firmware/target/hosted/{ctru/kernel-ctru.c => game_console/kernel-console.c} (87%) rename firmware/target/hosted/{ctru => game_console}/lcd-bitmap.h (100%) rename firmware/target/hosted/{ctru => game_console}/lcd-target.h (100%) rename firmware/target/hosted/{ctru/lib/bfile/bfile.c => game_console/lib/bfile/bfile-3ds.c} (84%) rename firmware/target/hosted/{ctru => game_console}/lib/bfile/bfile-internal.h (74%) create mode 100644 firmware/target/hosted/game_console/lib/bfile/bfile-rwmutex.c rename firmware/target/hosted/{ctru => game_console}/lib/bfile/bfile.h (100%) create mode 100644 firmware/target/hosted/game_console/lib/sys_atomic.c create mode 100644 firmware/target/hosted/game_console/lib/sys_atomic.h create mode 100644 firmware/target/hosted/game_console/lib/sys_cond_generic.c rename firmware/target/hosted/{ctru/lib/sys_dir.c => game_console/lib/sys_dir_3ds.c} (91%) rename firmware/target/hosted/{ctru => game_console}/lib/sys_file.h (87%) rename firmware/target/hosted/{ctru/lib/sys_file.c => game_console/lib/sys_file_3ds.c} (50%) create mode 100644 firmware/target/hosted/game_console/lib/sys_file_common.c create mode 100644 firmware/target/hosted/game_console/lib/sys_file_internal.h rename firmware/target/hosted/{ctru => game_console}/lib/sys_thread.h (68%) rename firmware/target/hosted/{ctru/lib/sys_thread.c => game_console/lib/sys_thread_3ds.c} (58%) rename firmware/target/hosted/{ctru => game_console}/lib/sys_timer.h (100%) create mode 100644 firmware/target/hosted/game_console/lib/sys_timer_3ds.c rename firmware/target/hosted/{ctru/lib/sys_timer.c => game_console/lib/sys_timer_common.c} (84%) create mode 100644 firmware/target/hosted/game_console/lib/sys_types.h rename firmware/target/hosted/{ctru/system-ctru.h => game_console/system-console.h} (100%) rename firmware/target/hosted/{ctru => game_console}/system-target.h (54%) rename firmware/target/hosted/{ctru/thread-ctru.c => game_console/thread-console.c} (82%) rename firmware/target/hosted/{ctru/thread-ctru.h => game_console/thread-console.h} (92%) rename firmware/target/hosted/{ctru/timer-ctru.c => game_console/timer-console.c} (100%) rename packaging/{ctru/ctru.make => 3ds/3ds.make} (93%) rename packaging/{ctru => 3ds}/res/app.rsf (100%) rename packaging/{ctru => 3ds}/res/banner.cgfx (100%) rename packaging/{ctru => 3ds}/res/banner.png (100%) rename packaging/{ctru => 3ds}/res/banner.wav (100%) rename packaging/{ctru => 3ds}/res/banner_.cgfx (100%) rename packaging/{ctru => 3ds}/res/icon.png (100%) rename packaging/{ctru => 3ds}/res/icon_yellow.png (100%) rename packaging/{ctru => 3ds}/rockbox.xml (100%) diff --git a/apps/SOURCES b/apps/SOURCES index bf39273405..f81261f766 100644 --- a/apps/SOURCES +++ b/apps/SOURCES @@ -300,8 +300,8 @@ keymaps/keymap-echor1.c keymaps/keymap-surfansf28.c #elif CONFIG_KEYPAD == RG_NANO_PAD keymaps/keymap-rgnano.c -#elif CONFIG_KEYPAD == CTRU_PAD -keymaps/keymap-ctru.c +#elif CONFIG_KEYPAD == N3DS_PAD +keymaps/keymap-3ds.c #elif CONFIG_KEYPAD == HIBY_R3PROII_PAD keymaps/keymap-hibyr3proii.c #elif CONFIG_KEYPAD == HIDIZS_AP80MAX_PAD diff --git a/apps/keymaps/keymap-ctru.c b/apps/keymaps/keymap-3ds.c similarity index 99% rename from apps/keymaps/keymap-ctru.c rename to apps/keymaps/keymap-3ds.c index 76f5e132d6..ab4f4c66c8 100644 --- a/apps/keymaps/keymap-ctru.c +++ b/apps/keymaps/keymap-3ds.c @@ -18,7 +18,7 @@ * ****************************************************************************/ -/* Button Code Definitions for Civic Type R (ctru) target */ +/* Button Code Definitions for the Nintendo 3DS target */ #include #include diff --git a/apps/plugins/SUBDIRS b/apps/plugins/SUBDIRS index 1ba38c97bb..b035fc9f50 100644 --- a/apps/plugins/SUBDIRS +++ b/apps/plugins/SUBDIRS @@ -74,7 +74,7 @@ mikmod (CONFIG_KEYPAD == SANSA_FUZE_PAD) || (CONFIG_KEYPAD == SANSA_E200_PAD) || \ (CONFIG_KEYPAD == IPOD_4G_PAD) || (CONFIG_KEYPAD == IPOD_3G_PAD) || \ (CONFIG_KEYPAD == IPOD_1G2G_PAD || CONFIG_KEYPAD == SAMSUNG_YPR0_PAD) || \ - (CONFIG_KEYPAD == SANSA_FUZEPLUS_PAD) || (CONFIG_KEYPAD == CTRU_PAD) || (CONFIG_KEYPAD == EROSQ_PAD) + (CONFIG_KEYPAD == SANSA_FUZEPLUS_PAD) || (CONFIG_KEYPAD == N3DS_PAD) || (CONFIG_KEYPAD == EROSQ_PAD) /* PDBox is confirmed to run on these player models. */ pdbox #endif diff --git a/apps/plugins/battery_bench.c b/apps/plugins/battery_bench.c index 4b2d90588c..5a129c6136 100644 --- a/apps/plugins/battery_bench.c +++ b/apps/plugins/battery_bench.c @@ -258,7 +258,7 @@ struct battery_tables_t { #define BATTERY_ON_TXT "A" #define BATTERY_OFF_TXT "X" -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define BATTERY_ON BUTTON_SELECT #define BATTERY_OFF BUTTON_BACK #define BATTERY_ON_TXT "A - start" diff --git a/apps/plugins/blackjack.c b/apps/plugins/blackjack.c index 233c3e760d..d90fed287f 100644 --- a/apps/plugins/blackjack.c +++ b/apps/plugins/blackjack.c @@ -611,7 +611,7 @@ enum { #define BJACK_RIGHT BUTTON_RIGHT #define BJACK_LEFT BUTTON_LEFT -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define BJACK_SELECT_NAME "A" #define BJACK_STAY_NAME "X" #define BJACK_QUIT_NAME "B" diff --git a/apps/plugins/brickmania.c b/apps/plugins/brickmania.c index b588000cdb..08a6e36a3e 100644 --- a/apps/plugins/brickmania.c +++ b/apps/plugins/brickmania.c @@ -373,7 +373,7 @@ CONFIG_KEYPAD == SANSA_M200_PAD #define UP BUTTON_UP #define DOWN BUTTON_DOWN -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define QUIT BUTTON_BACK #define LEFT BUTTON_LEFT #define RIGHT BUTTON_RIGHT diff --git a/apps/plugins/calculator.c b/apps/plugins/calculator.c index 252618865c..a42564dd2a 100644 --- a/apps/plugins/calculator.c +++ b/apps/plugins/calculator.c @@ -546,7 +546,7 @@ F3: equal to "=" #define CALCULATOR_CALC BUTTON_X #define CALCULATOR_CLEAR BUTTON_B -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define CALCULATOR_LEFT BUTTON_LEFT #define CALCULATOR_RIGHT BUTTON_RIGHT diff --git a/apps/plugins/calendar.c b/apps/plugins/calendar.c index 2b6813c3f4..37f3d75d33 100644 --- a/apps/plugins/calendar.c +++ b/apps/plugins/calendar.c @@ -432,7 +432,7 @@ #define CALENDAR_PREV_MONTH BUTTON_L #define CALENDAR_EVENT_MENU_NAME "A" -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define CALENDAR_QUIT BUTTON_BACK #define CALENDAR_SELECT BUTTON_SELECT #define CALENDAR_NEXT_WEEK BUTTON_DOWN diff --git a/apps/plugins/chessbox/chessbox_pgn.h b/apps/plugins/chessbox/chessbox_pgn.h index d046b1ed8b..f4f505ef20 100644 --- a/apps/plugins/chessbox/chessbox_pgn.h +++ b/apps/plugins/chessbox/chessbox_pgn.h @@ -599,7 +599,7 @@ #define CB_SCROLL_RIGHT (BUTTON_RIGHT|BUTTON_REPEAT) #define CB_RC_QUIT BUTTON_START -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define CB_SELECT BUTTON_SELECT #define CB_UP BUTTON_UP #define CB_DOWN BUTTON_DOWN diff --git a/apps/plugins/chessclock.c b/apps/plugins/chessclock.c index fd9bc92708..971af2d90e 100644 --- a/apps/plugins/chessclock.c +++ b/apps/plugins/chessclock.c @@ -407,7 +407,7 @@ #define CHC_SETTINGS_OK BUTTON_A #define CHC_SETTINGS_CANCEL BUTTON_B -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define CHC_QUIT BUTTON_BACK #define CHC_STARTSTOP BUTTON_SELECT #define CHC_RESET BUTTON_USER diff --git a/apps/plugins/chip8.c b/apps/plugins/chip8.c index df583bb04c..bf22fcb0b9 100644 --- a/apps/plugins/chip8.c +++ b/apps/plugins/chip8.c @@ -1301,7 +1301,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define CHIP8_KEY9 BUTTON_R #define CHIP8_KEY0 BUTTON_L -#elif (CONFIG_KEYPAD == CTRU_PAD) +#elif (CONFIG_KEYPAD == N3DS_PAD) #define CHIP8_OFF (BUTTON_BACK|BUTTON_REPEAT) #define CHIP8_KEY1 BUTTON_MENU #define CHIP8_KEY2 BUTTON_UP diff --git a/apps/plugins/chopper.c b/apps/plugins/chopper.c index 8dfa68a2b8..cb4816c2f6 100644 --- a/apps/plugins/chopper.c +++ b/apps/plugins/chopper.c @@ -206,7 +206,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define ACTION2 BUTTON_B #define ACTIONTEXT "A" -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define QUIT BUTTON_BACK #define ACTION BUTTON_SELECT #define ACTION2 BUTTON_MENU diff --git a/apps/plugins/clix.c b/apps/plugins/clix.c index 356021f826..6309cf1e83 100644 --- a/apps/plugins/clix.c +++ b/apps/plugins/clix.c @@ -324,7 +324,7 @@ #define CLIX_BUTTON_UP BUTTON_UP #define CLIX_BUTTON_DOWN BUTTON_DOWN -#elif (CONFIG_KEYPAD == CTRU_PAD) +#elif (CONFIG_KEYPAD == N3DS_PAD) #define CLIX_BUTTON_QUIT BUTTON_BACK #define CLIX_BUTTON_LEFT BUTTON_LEFT #define CLIX_BUTTON_RIGHT BUTTON_RIGHT diff --git a/apps/plugins/cube.c b/apps/plugins/cube.c index 9a48b37a37..3936bca565 100644 --- a/apps/plugins/cube.c +++ b/apps/plugins/cube.c @@ -414,7 +414,7 @@ #define CUBE_PAUSE BUTTON_A #define CUBE_HIGHSPEED BUTTON_Y -#elif (CONFIG_KEYPAD == CTRU_PAD) +#elif (CONFIG_KEYPAD == N3DS_PAD) #define CUBE_QUIT BUTTON_BACK #define CUBE_NEXT BUTTON_RIGHT #define CUBE_PREV BUTTON_LEFT diff --git a/apps/plugins/doom/i_video.c b/apps/plugins/doom/i_video.c index 5f3eb68cbe..e5cca93d31 100644 --- a/apps/plugins/doom/i_video.c +++ b/apps/plugins/doom/i_video.c @@ -632,7 +632,7 @@ void I_ShutdownGraphics(void) #define DOOMBUTTON_WEAPON BUTTON_X #define DOOMBUTTON_MAP BUTTON_Y -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define DOOMBUTTON_UP BUTTON_UP #define DOOMBUTTON_DOWN BUTTON_DOWN #define DOOMBUTTON_LEFT BUTTON_LEFT diff --git a/apps/plugins/flipit.c b/apps/plugins/flipit.c index 673dadbb3e..c33a22a709 100644 --- a/apps/plugins/flipit.c +++ b/apps/plugins/flipit.c @@ -510,7 +510,7 @@ #define FLIPIT_STEP_BY_STEP BUTTON_Y #define FLIPIT_TOGGLE BUTTON_A -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define FLIPIT_LEFT BUTTON_LEFT #define FLIPIT_RIGHT BUTTON_RIGHT diff --git a/apps/plugins/fractals/fractal.h b/apps/plugins/fractals/fractal.h index 84a932d713..f1f5bee424 100644 --- a/apps/plugins/fractals/fractal.h +++ b/apps/plugins/fractals/fractal.h @@ -518,7 +518,7 @@ #define FRACTAL_PRECISION_DEC BUTTON_L #define FRACTAL_RESET BUTTON_A -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define FRACTAL_QUIT BUTTON_BACK #define FRACTAL_UP BUTTON_UP #define FRACTAL_DOWN BUTTON_DOWN diff --git a/apps/plugins/goban/goban.h b/apps/plugins/goban/goban.h index 4ecabcca76..d6c68178ae 100644 --- a/apps/plugins/goban/goban.h +++ b/apps/plugins/goban/goban.h @@ -509,7 +509,7 @@ #define GBN_BUTTON_CONTEXT BUTTON_X #define GBN_BUTTON_NEXT_VAR BUTTON_Y -#elif (CONFIG_KEYPAD == CTRU_PAD) +#elif (CONFIG_KEYPAD == N3DS_PAD) #define GBN_BUTTON_UP BUTTON_UP #define GBN_BUTTON_DOWN BUTTON_DOWN #define GBN_BUTTON_LEFT BUTTON_LEFT diff --git a/apps/plugins/imageviewer/imageviewer_button.h b/apps/plugins/imageviewer/imageviewer_button.h index cbfce5bc9a..7eeb41df86 100644 --- a/apps/plugins/imageviewer/imageviewer_button.h +++ b/apps/plugins/imageviewer/imageviewer_button.h @@ -559,7 +559,7 @@ #define IMGVIEW_MENU BUTTON_B #define IMGVIEW_QUIT BUTTON_START -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define IMGVIEW_ZOOM_IN BUTTON_POWER #define IMGVIEW_ZOOM_OUT BUTTON_USER #define IMGVIEW_UP BUTTON_UP diff --git a/apps/plugins/invadrox.c b/apps/plugins/invadrox.c index 294b4b3c3f..3b12307d0e 100644 --- a/apps/plugins/invadrox.c +++ b/apps/plugins/invadrox.c @@ -297,7 +297,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define RIGHT BUTTON_RIGHT #define FIRE BUTTON_A -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define QUIT BUTTON_POWER #define LEFT BUTTON_LEFT diff --git a/apps/plugins/jewels.c b/apps/plugins/jewels.c index ce2de05330..02953847b1 100644 --- a/apps/plugins/jewels.c +++ b/apps/plugins/jewels.c @@ -389,7 +389,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define HK_SELECT "A" #define HK_CANCEL "START" -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define JEWELS_UP BUTTON_UP #define JEWELS_DOWN BUTTON_DOWN #define JEWELS_LEFT BUTTON_LEFT diff --git a/apps/plugins/lib/keymaps.h b/apps/plugins/lib/keymaps.h index 51e9f65395..b67a7aeabe 100644 --- a/apps/plugins/lib/keymaps.h +++ b/apps/plugins/lib/keymaps.h @@ -274,7 +274,7 @@ #define BTN_FIRE BUTTON_A #define BTN_PAUSE BUTTON_START -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define BTN_UP BUTTON_UP #define BTN_DOWN BUTTON_DOWN #define BTN_LEFT BUTTON_LEFT @@ -319,7 +319,7 @@ (CONFIG_KEYPAD != SHANLING_Q1_PAD) && \ (CONFIG_KEYPAD != HIBY_R3PROII_PAD) && \ (CONFIG_KEYPAD != HIDIZS_AP80MAX_PAD) && \ - (CONFIG_KEYPAD != CTRU_PAD) + (CONFIG_KEYPAD != N3DS_PAD) #define BTN_FIRE BUTTON_BOTTOMLEFT #define BTN_PAUSE BUTTON_TOPLEFT #endif diff --git a/apps/plugins/lib/pluginlib_actions.c b/apps/plugins/lib/pluginlib_actions.c index a45a0a0399..6dadfbc784 100644 --- a/apps/plugins/lib/pluginlib_actions.c +++ b/apps/plugins/lib/pluginlib_actions.c @@ -284,7 +284,7 @@ const struct button_mapping pla_main_ctx[] = { PLA_DOWN_REPEAT, BUTTON_DOWN|BUTTON_REPEAT, BUTTON_NONE }, { PLA_LEFT_REPEAT, BUTTON_LEFT|BUTTON_REPEAT, BUTTON_NONE }, { PLA_RIGHT_REPEAT, BUTTON_RIGHT|BUTTON_REPEAT, BUTTON_NONE }, -#elif (CONFIG_KEYPAD == CTRU_PAD) +#elif (CONFIG_KEYPAD == N3DS_PAD) { PLA_UP, BUTTON_UP, BUTTON_NONE }, { PLA_DOWN, BUTTON_DOWN, BUTTON_NONE }, { PLA_LEFT, BUTTON_LEFT, BUTTON_NONE }, @@ -543,7 +543,7 @@ const struct button_mapping pla_main_ctx[] = {PLA_SELECT, BUTTON_A, BUTTON_NONE}, {PLA_SELECT_REL, BUTTON_A|BUTTON_REL, BUTTON_A}, {PLA_SELECT_REPEAT, BUTTON_A|BUTTON_REPEAT, BUTTON_NONE}, -#elif (CONFIG_KEYPAD == CTRU_PAD) +#elif (CONFIG_KEYPAD == N3DS_PAD) {PLA_CANCEL, BUTTON_BACK, BUTTON_NONE}, {PLA_EXIT, BUTTON_MENU, BUTTON_NONE}, {PLA_SELECT, BUTTON_SELECT, BUTTON_NONE}, diff --git a/apps/plugins/midi/midiplay.c b/apps/plugins/midi/midiplay.c index 647273bf4a..ee5b1f13f9 100644 --- a/apps/plugins/midi/midiplay.c +++ b/apps/plugins/midi/midiplay.c @@ -335,7 +335,7 @@ #define MIDI_VOL_DOWN BUTTON_DOWN #define MIDI_PLAYPAUSE BUTTON_A -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define MIDI_QUIT BUTTON_BACK #define MIDI_FFWD BUTTON_RIGHT #define MIDI_REWIND BUTTON_LEFT diff --git a/apps/plugins/mikmod/mikmod.c b/apps/plugins/mikmod/mikmod.c index a482ace68b..81bc2c4413 100644 --- a/apps/plugins/mikmod/mikmod.c +++ b/apps/plugins/mikmod/mikmod.c @@ -6,7 +6,7 @@ #undef SYNC -#if defined(SIMULATOR) || defined(CTRU) +#if defined(SIMULATOR) || defined(N3DS) #define SYNC #else #define USETHREADS diff --git a/apps/plugins/minesweeper.c b/apps/plugins/minesweeper.c index 704c659a40..6e50e12ab0 100644 --- a/apps/plugins/minesweeper.c +++ b/apps/plugins/minesweeper.c @@ -454,7 +454,7 @@ CONFIG_KEYPAD == MROBE500_PAD # define MINESWP_DISCOVER BUTTON_A # define MINESWP_INFO BUTTON_Y -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD # define MINESWP_LEFT BUTTON_LEFT # define MINESWP_RIGHT BUTTON_RIGHT # define MINESWP_UP BUTTON_UP diff --git a/apps/plugins/mp3_encoder.c b/apps/plugins/mp3_encoder.c index f6423a4329..9a8acecf88 100644 --- a/apps/plugins/mp3_encoder.c +++ b/apps/plugins/mp3_encoder.c @@ -2593,7 +2593,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define MP3ENC_DONE BUTTON_START #define MP3ENC_SELECT BUTTON_A -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define MP3ENC_PREV BUTTON_UP #define MP3ENC_NEXT BUTTON_DOWN #define MP3ENC_DONE BUTTON_POWER diff --git a/apps/plugins/mpegplayer/mpeg_settings.c b/apps/plugins/mpegplayer/mpeg_settings.c index 8475f26b7f..c3db0336ec 100644 --- a/apps/plugins/mpegplayer/mpeg_settings.c +++ b/apps/plugins/mpegplayer/mpeg_settings.c @@ -365,7 +365,7 @@ struct mpeg_settings settings; #define MPEG_START_TIME_DOWN BUTTON_DOWN #define MPEG_START_TIME_EXIT BUTTON_START -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define MPEG_START_TIME_SELECT BUTTON_SELECT #define MPEG_START_TIME_LEFT BUTTON_LEFT #define MPEG_START_TIME_RIGHT BUTTON_RIGHT diff --git a/apps/plugins/mpegplayer/mpegplayer.c b/apps/plugins/mpegplayer/mpegplayer.c index 761c95cde0..869d691574 100644 --- a/apps/plugins/mpegplayer/mpegplayer.c +++ b/apps/plugins/mpegplayer/mpegplayer.c @@ -497,7 +497,7 @@ CONFIG_KEYPAD == SANSA_M200_PAD #define MPEG_RW BUTTON_LEFT #define MPEG_FF BUTTON_RIGHT -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define MPEG_MENU BUTTON_MENU #define MPEG_PAUSE BUTTON_SELECT #define MPEG_STOP BUTTON_POWER diff --git a/apps/plugins/oscilloscope.c b/apps/plugins/oscilloscope.c index c0bef75cd4..d8b3c74eaa 100644 --- a/apps/plugins/oscilloscope.c +++ b/apps/plugins/oscilloscope.c @@ -564,7 +564,7 @@ #define OSCILLOSCOPE_VOL_UP BUTTON_UP #define OSCILLOSCOPE_VOL_DOWN BUTTON_DOWN -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define OSCILLOSCOPE_QUIT BUTTON_BACK #define OSCILLOSCOPE_DRAWMODE BUTTON_USER #define OSCILLOSCOPE_ADVMODE BUTTON_MENU diff --git a/apps/plugins/pacbox/pacbox.h b/apps/plugins/pacbox/pacbox.h index 855e3c536c..86fbeb8ef1 100644 --- a/apps/plugins/pacbox/pacbox.h +++ b/apps/plugins/pacbox/pacbox.h @@ -417,7 +417,7 @@ #define PACMAN_COIN BUTTON_A #define PACMAN_MENU BUTTON_START -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define PACMAN_UP BUTTON_UP #define PACMAN_DOWN BUTTON_DOWN diff --git a/apps/plugins/pdbox/pdbox.h b/apps/plugins/pdbox/pdbox.h index 2fd628fea2..e1144b5f22 100644 --- a/apps/plugins/pdbox/pdbox.h +++ b/apps/plugins/pdbox/pdbox.h @@ -306,7 +306,7 @@ enum pd_key_id #define PDPOD_WHEELLEFT BUTTON_UP #define PDPOD_WHEELRIGHT BUTTON_DOWN #define PDPOD_ACTION BUTTON_SELECT -#elif (CONFIG_KEYPAD == CTRU_PAD) +#elif (CONFIG_KEYPAD == N3DS_PAD) #define PDPOD_QUIT BUTTON_BACK #define PDPOD_PLAY BUTTON_USER #define PDPOD_PREVIOUS BUTTON_LEFT diff --git a/apps/plugins/pegbox.c b/apps/plugins/pegbox.c index bd17965ef6..3572320a3f 100644 --- a/apps/plugins/pegbox.c +++ b/apps/plugins/pegbox.c @@ -730,7 +730,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define LVL_UP_TEXT "R" #define LVL_DOWN_TEXT "L" -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define PEGBOX_SELECT BUTTON_SELECT #define PEGBOX_QUIT BUTTON_BACK #define PEGBOX_RESTART BUTTON_MENU diff --git a/apps/plugins/plugin_crt0.c b/apps/plugins/plugin_crt0.c index a9270c4fa0..f64becec2e 100644 --- a/apps/plugins/plugin_crt0.c +++ b/apps/plugins/plugin_crt0.c @@ -24,7 +24,7 @@ PLUGIN_HEADER -#ifdef CTRU +#ifdef N3DS /* dummy undefined symbols to build plugins for ctru */ void __aeabi_unwind_cpp_pr0(void) {} struct _reent * _EXFUN(__getreent, (void)) { return NULL; } diff --git a/apps/plugins/pong.c b/apps/plugins/pong.c index 5cc79de1d1..d044563cd7 100644 --- a/apps/plugins/pong.c +++ b/apps/plugins/pong.c @@ -336,7 +336,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define PONG_RIGHT_UP BUTTON_X #define PONG_RIGHT_DOWN BUTTON_Y -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define PONG_QUIT BUTTON_BACK #define PONG_PAUSE BUTTON_SELECT #define PONG_LEFT_UP BUTTON_UP diff --git a/apps/plugins/reversi/reversi-gui.h b/apps/plugins/reversi/reversi-gui.h index 79755f620a..cc8b305825 100644 --- a/apps/plugins/reversi/reversi-gui.h +++ b/apps/plugins/reversi/reversi-gui.h @@ -372,7 +372,7 @@ #define REVERSI_BUTTON_MAKE_MOVE BUTTON_A #define REVERSI_BUTTON_MENU BUTTON_B -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define REVERSI_BUTTON_QUIT BUTTON_BACK #define REVERSI_BUTTON_UP BUTTON_UP #define REVERSI_BUTTON_DOWN BUTTON_DOWN diff --git a/apps/plugins/rockblox.c b/apps/plugins/rockblox.c index b310cf65bd..ac23ff7b6f 100644 --- a/apps/plugins/rockblox.c +++ b/apps/plugins/rockblox.c @@ -482,7 +482,7 @@ #define ROCKBLOX_DROP BUTTON_A #define ROCKBLOX_RESTART BUTTON_B -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define ROCKBLOX_OFF BUTTON_BACK #define ROCKBLOX_ROTATE_CCW BUTTON_POWER diff --git a/apps/plugins/rockboy/rockboy.c b/apps/plugins/rockboy/rockboy.c index 7d7c9f1b01..3c4bcb829d 100644 --- a/apps/plugins/rockboy/rockboy.c +++ b/apps/plugins/rockboy/rockboy.c @@ -482,7 +482,7 @@ static void setoptions (void) options.SELECT = BUTTON_FN; options.MENU = BUTTON_X; -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD options.UP = BUTTON_UP; options.DOWN = BUTTON_DOWN; options.LEFT = BUTTON_LEFT; @@ -510,7 +510,7 @@ static void setoptions (void) #error No Keymap Defined! #endif -#if defined(HAVE_TOUCHSCREEN) && (CONFIG_KEYPAD != CTRU_PAD) +#if defined(HAVE_TOUCHSCREEN) && (CONFIG_KEYPAD != N3DS_PAD) options.UP = BUTTON_TOPMIDDLE; options.DOWN = BUTTON_BOTTOMMIDDLE; options.START = BUTTON_TOPRIGHT; diff --git a/apps/plugins/rockpaint.c b/apps/plugins/rockpaint.c index 7ed5b4104a..b0731afb38 100644 --- a/apps/plugins/rockpaint.c +++ b/apps/plugins/rockpaint.c @@ -428,7 +428,7 @@ #define ROCKPAINT_LEFT BUTTON_LEFT #define ROCKPAINT_RIGHT BUTTON_RIGHT -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define ROCKPAINT_QUIT BUTTON_BACK #define ROCKPAINT_DRAW BUTTON_SELECT #define ROCKPAINT_MENU BUTTON_MENU diff --git a/apps/plugins/sdl/sdl.make b/apps/plugins/sdl/sdl.make index 8b27c49341..325b430028 100644 --- a/apps/plugins/sdl/sdl.make +++ b/apps/plugins/sdl/sdl.make @@ -38,8 +38,8 @@ SDLFLAGS = -I$(SDL_SRCDIR)/include $(filter-out -O%,$(PLUGINFLAGS)) \ #-ffast-math -funroll-loops -fomit-frame-pointer -fexpensive-optimizations \ #-D_GNU_SOURCE=1 -D_REENTRANT -DSDL -DELF -# CTRU does need these to avoid compiler errors -ifeq ($(APP_TYPE),ctru-app) +# N3DS does need these to avoid compiler errors +ifeq ($(APP_TYPE),3ds-homebrew) SDLFLAGS += -Wno-int-conversion -Wno-incompatible-pointer-types \ -Wno-implicit-function-declaration -Wno-implicit-int endif diff --git a/apps/plugins/sliding_puzzle.c b/apps/plugins/sliding_puzzle.c index 562384ac6a..b3a940bfda 100644 --- a/apps/plugins/sliding_puzzle.c +++ b/apps/plugins/sliding_puzzle.c @@ -368,7 +368,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define PUZZLE_SHUFFLE BUTTON_B #define PUZZLE_PICTURE BUTTON_A -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define PUZZLE_QUIT BUTTON_BACK #define PUZZLE_LEFT BUTTON_LEFT #define PUZZLE_RIGHT BUTTON_RIGHT diff --git a/apps/plugins/snake.c b/apps/plugins/snake.c index 2ae29ee6c0..3eb08271cb 100644 --- a/apps/plugins/snake.c +++ b/apps/plugins/snake.c @@ -323,7 +323,7 @@ dir is the current direction of the snake - 0=up, 1=right, 2=down, 3=left; #define SNAKE_DOWN BUTTON_DOWN #define SNAKE_PLAYPAUSE BUTTON_A -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define SNAKE_QUIT BUTTON_BACK #define SNAKE_LEFT BUTTON_LEFT #define SNAKE_RIGHT BUTTON_RIGHT diff --git a/apps/plugins/snake2.c b/apps/plugins/snake2.c index 6b225713e4..cc8eb06e35 100644 --- a/apps/plugins/snake2.c +++ b/apps/plugins/snake2.c @@ -469,7 +469,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define SNAKE2_PLAYPAUSE BUTTON_A #define SNAKE2_PLAYPAUSE_TEXT "A" -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define SNAKE2_LEFT BUTTON_LEFT #define SNAKE2_RIGHT BUTTON_RIGHT #define SNAKE2_UP BUTTON_UP diff --git a/apps/plugins/sokoban.c b/apps/plugins/sokoban.c index 625a2679fd..5a539fd9ef 100644 --- a/apps/plugins/sokoban.c +++ b/apps/plugins/sokoban.c @@ -712,7 +712,7 @@ #define BUTTON_SAVE BUTTON_A #define BUTTON_SAVE_NAME "A" -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define SOKOBAN_LEFT BUTTON_LEFT #define SOKOBAN_RIGHT BUTTON_RIGHT #define SOKOBAN_UP BUTTON_UP diff --git a/apps/plugins/solitaire.c b/apps/plugins/solitaire.c index abe2143ef8..17e24cbc54 100644 --- a/apps/plugins/solitaire.c +++ b/apps/plugins/solitaire.c @@ -742,7 +742,7 @@ CONFIG_KEYPAD == MROBE500_PAD # define HK_CUR2STACK "B" # define HK_REM2STACK "R" -#elif (CONFIG_KEYPAD == CTRU_PAD) +#elif (CONFIG_KEYPAD == N3DS_PAD) # define SOL_QUIT BUTTON_BACK # define SOL_UP BUTTON_UP # define SOL_DOWN BUTTON_DOWN diff --git a/apps/plugins/spacerocks.c b/apps/plugins/spacerocks.c index 95338d0ef9..56bae2125b 100644 --- a/apps/plugins/spacerocks.c +++ b/apps/plugins/spacerocks.c @@ -383,7 +383,7 @@ #define AST_RIGHT BUTTON_RIGHT #define AST_FIRE BUTTON_A -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define AST_PAUSE BUTTON_MENU #define AST_QUIT BUTTON_BACK #define AST_THRUST BUTTON_UP diff --git a/apps/plugins/star.c b/apps/plugins/star.c index 86e95fc2ce..92b56447d2 100644 --- a/apps/plugins/star.c +++ b/apps/plugins/star.c @@ -687,7 +687,7 @@ #define STAR_LEVEL_DOWN_NAME "L" #define STAR_LEVEL_REPEAT_NAME "B" -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define STAR_QUIT BUTTON_BACK #define STAR_LEFT BUTTON_LEFT diff --git a/apps/plugins/stopwatch.c b/apps/plugins/stopwatch.c index 550904efd9..87b1857921 100644 --- a/apps/plugins/stopwatch.c +++ b/apps/plugins/stopwatch.c @@ -305,7 +305,7 @@ #define STOPWATCH_SCROLL_UP BUTTON_UP #define STOPWATCH_SCROLL_DOWN BUTTON_DOWN -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define STOPWATCH_QUIT BUTTON_BACK #define STOPWATCH_START_STOP BUTTON_SELECT #define STOPWATCH_RESET_TIMER BUTTON_MENU diff --git a/apps/plugins/sudoku/sudoku.h b/apps/plugins/sudoku/sudoku.h index 8926bd68af..410d15acf0 100644 --- a/apps/plugins/sudoku/sudoku.h +++ b/apps/plugins/sudoku/sudoku.h @@ -472,7 +472,7 @@ #define SUDOKU_BUTTON_MENU BUTTON_B #define SUDOKU_BUTTON_POSSIBLE BUTTON_X -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define SUDOKU_BUTTON_QUIT BUTTON_BACK #define SUDOKU_BUTTON_UP BUTTON_UP #define SUDOKU_BUTTON_DOWN BUTTON_DOWN diff --git a/apps/plugins/text_viewer/tv_button.h b/apps/plugins/text_viewer/tv_button.h index e64ba00e58..c42afdc91b 100644 --- a/apps/plugins/text_viewer/tv_button.h +++ b/apps/plugins/text_viewer/tv_button.h @@ -587,7 +587,7 @@ #define TV_LINE_DOWN BUTTON_L #define TV_BOOKMARK BUTTON_A -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define TV_QUIT BUTTON_BACK #define TV_SCROLL_UP BUTTON_UP #define TV_SCROLL_DOWN BUTTON_DOWN diff --git a/apps/plugins/vu_meter.c b/apps/plugins/vu_meter.c index 22f5fe4b31..1789b9bb89 100644 --- a/apps/plugins/vu_meter.c +++ b/apps/plugins/vu_meter.c @@ -468,7 +468,7 @@ #define LABEL_MENU "B" #define LABEL_VOLUME "UP/DOWN" -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define VUMETER_QUIT BUTTON_BACK #define VUMETER_HELP BUTTON_USER #define VUMETER_MENU BUTTON_MENU diff --git a/apps/plugins/wormlet.c b/apps/plugins/wormlet.c index af1e0c9662..065769e7ea 100644 --- a/apps/plugins/wormlet.c +++ b/apps/plugins/wormlet.c @@ -418,7 +418,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define BTN_QUIT BUTTON_START #define BTN_STOPRESET BUTTON_B -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define BTN_DIR_UP BUTTON_UP #define BTN_DIR_DOWN BUTTON_DOWN diff --git a/apps/plugins/xobox.c b/apps/plugins/xobox.c index 12c9c4fd2b..d29ac579b2 100644 --- a/apps/plugins/xobox.c +++ b/apps/plugins/xobox.c @@ -362,7 +362,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define DOWN BUTTON_DOWN #define PAUSE BUTTON_A -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define QUIT BUTTON_BACK #define LEFT BUTTON_LEFT diff --git a/apps/plugins/zxbox/keymaps.h b/apps/plugins/zxbox/keymaps.h index e87c6afff3..d2ffb33fe6 100644 --- a/apps/plugins/zxbox/keymaps.h +++ b/apps/plugins/zxbox/keymaps.h @@ -301,7 +301,7 @@ #define ZX_SELECT BUTTON_A #define ZX_MENU BUTTON_START -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define ZX_UP BUTTON_UP #define ZX_DOWN BUTTON_DOWN diff --git a/apps/plugins/zxbox/zxbox_keyb.c b/apps/plugins/zxbox/zxbox_keyb.c index 053215ce6d..55629d4f60 100644 --- a/apps/plugins/zxbox/zxbox_keyb.c +++ b/apps/plugins/zxbox/zxbox_keyb.c @@ -291,7 +291,7 @@ #define KBD_UP BUTTON_UP #define KBD_DOWN BUTTON_DOWN -#elif CONFIG_KEYPAD == CTRU_PAD +#elif CONFIG_KEYPAD == N3DS_PAD #define KBD_SELECT BUTTON_SELECT #define KBD_ABORT BUTTON_BACK diff --git a/apps/tree.c b/apps/tree.c index 7f3bee1113..85cf14e465 100644 --- a/apps/tree.c +++ b/apps/tree.c @@ -570,7 +570,7 @@ void resume_directory(const char *dir) /* Returns the current working directory and also writes cwd to buf if non-NULL. In case of error, returns NULL. */ -#ifdef CTRU +#ifdef N3DS char *__wrap_getcwd(char *buf, getcwd_size_t size) #else char *getcwd(char *buf, getcwd_size_t size) diff --git a/apps/tree.h b/apps/tree.h index cf8b4833c1..7d646ffed5 100644 --- a/apps/tree.h +++ b/apps/tree.h @@ -127,7 +127,7 @@ void tree_unlock_cache(struct tree_context *t); #else #define getcwd_size_t size_t #endif -#ifdef CTRU +#ifdef N3DS /* devkitarm already defines getcwd */ char *__wrap_getcwd(char *buf, getcwd_size_t size); #else diff --git a/firmware/SOURCES b/firmware/SOURCES index 08d81c6edc..f7bcd5d2b9 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -25,7 +25,7 @@ target/hosted/rtc.c #if (CONFIG_PLATFORM & PLATFORM_ANDROID) == 0 && \ !defined(DX50) && !defined(DX90) && \ - (defined(DEBUG) || defined(SIMULATOR) || defined(CTRU)) /* sim should define DEBUG instead */ + (defined(DEBUG) || defined(SIMULATOR) || defined(N3DS)) /* sim should define DEBUG instead */ target/hosted/debug-hosted.c #endif @@ -101,14 +101,14 @@ target/hosted/sdl/app/button-application.c #ifdef WIN32 target/hosted/filesystem-win32.c #else /* !WIN32 */ -#ifndef CTRU +#if !(CONFIG_PLATFORM & PLATFORM_GAME_CONSOLE) target/hosted/filesystem-unix.c -#endif /* CTRU */ +#endif /* PLATFORM_GAME_CONSOLE */ #endif /* WIN32 */ #endif /* APPLICATION */ #endif /* HAVE_SDL */ -#if defined(APPLICATION) && !defined(CTRU) +#if defined(APPLICATION) && !(CONFIG_PLATFORM & PLATFORM_GAME_CONSOLE) target/hosted/filesystem-app.c #endif /* APPLICATION */ @@ -624,8 +624,8 @@ target/hosted/sdl/pcm-sdl.c drivers/audio/as3514.c #elif defined(SAMSUNG_YPR1) && defined(HAVE_WM8978) drivers/audio/wm8978.c -#elif defined(CTRU) -drivers/audio/ctru.c +#elif defined(N3DS) +drivers/audio/3ds.c #endif #endif /* (CONFIG_PLATFORM & PLATFORM_NATIVE) */ #endif /* !defined(BOOTLOADER) */ @@ -2046,26 +2046,35 @@ target/hosted/tinyalsa/mixer.c target/hosted/tinyalsa/pcm.c #endif -#if (CONFIG_PLATFORM & PLATFORM_CTRU) +#if (CONFIG_PLATFORM & PLATFORM_GAME_CONSOLE) +#ifdef CPU_ARM asm/arm/lcd-as-memframe.S -target/hosted/ctru/backlight-ctru.c -target/hosted/ctru/button-ctru.c -target/hosted/ctru/kernel-ctru.c -target/hosted/ctru/thread-ctru.c -target/hosted/ctru/lcd-bitmap.c -target/hosted/ctru/luminance-ctru.c -target/hosted/ctru/system-ctru.c -target/hosted/ctru/filesystem-ctru.c -target/hosted/ctru/lc-ctru.c -target/hosted/ctru/lc-program-resolver.c -target/hosted/ctru/powermgmt-ctru.c -target/hosted/ctru/timer-ctru.c -target/hosted/ctru/pcm-ctru.c -target/hosted/ctru/lib/sys_file.c -target/hosted/ctru/lib/sys_dir.c -target/hosted/ctru/lib/sys_thread.c -target/hosted/ctru/lib/sys_timer.c -target/hosted/ctru/lib/bfile/bfile.c +#endif +target/hosted/game_console/kernel-console.c +target/hosted/game_console/thread-console.c +target/hosted/game_console/timer-console.c +target/hosted/game_console/lib/sys_atomic.c +target/hosted/game_console/lib/sys_file_common.c +target/hosted/game_console/lib/sys_timer_common.c +target/hosted/game_console/lib/bfile/bfile-rwmutex.c +target/hosted/game_console/lib/sys_cond_generic.c +#ifdef N3DS +target/hosted/game_console/lib/sys_file_3ds.c +target/hosted/game_console/lib/sys_dir_3ds.c +target/hosted/game_console/lib/sys_thread_3ds.c +target/hosted/game_console/lib/sys_timer_3ds.c +target/hosted/game_console/lib/bfile/bfile-3ds.c +target/hosted/game_console/3ds/backlight-3ds.c +target/hosted/game_console/3ds/button-3ds.c +target/hosted/game_console/3ds/filesystem-3ds.c +target/hosted/game_console/3ds/lc-3ds.c +target/hosted/game_console/3ds/lcd-bitmap-3ds.c +target/hosted/game_console/3ds/lc-program-resolver.c +target/hosted/game_console/3ds/luminance-3ds.c +target/hosted/game_console/3ds/pcm-3ds.c +target/hosted/game_console/3ds/powermgmt-3ds.c +target/hosted/game_console/3ds/system-3ds.c +#endif /* N3DS */ #endif #else /* defined(SIMULATOR) */ @@ -2122,7 +2131,7 @@ kernel/semaphore.c #endif #ifdef HAVE_SDL_THREADS target/hosted/sdl/thread-sdl.c -#elif !defined(CTRU) +#elif !(CONFIG_PLATFORM & PLATFORM_GAME_CONSOLE) kernel/thread.c #endif kernel/thread-common.c diff --git a/firmware/drivers/audio/ctru.c b/firmware/drivers/audio/3ds.c similarity index 100% rename from firmware/drivers/audio/ctru.c rename to firmware/drivers/audio/3ds.c diff --git a/firmware/export/audiohw.h b/firmware/export/audiohw.h index 75e65e05a4..6525cc0452 100644 --- a/firmware/export/audiohw.h +++ b/firmware/export/audiohw.h @@ -222,7 +222,7 @@ struct sound_settings_info #include "es9218.h" #elif (CONFIG_PLATFORM & PLATFORM_ANDROID) #include "android_codec.h" -#elif ((CONFIG_PLATFORM & (PLATFORM_SDL | PLATFORM_CTRU)) | defined(RG_NANO)) +#elif ((CONFIG_PLATFORM & (PLATFORM_SDL | PLATFORM_GAME_CONSOLE)) | defined(RG_NANO)) #include "sdl_codec.h" #elif defined(DX50) #include "codec-dx50.h" diff --git a/firmware/export/config.h b/firmware/export/config.h index 1a70ad7bdb..c826aa513f 100644 --- a/firmware/export/config.h +++ b/firmware/export/config.h @@ -88,11 +88,11 @@ * bit fields to allow PLATFORM_HOSTED to be OR'ed e.g. with a * possible future PLATFORM_ANDROID (some OSes might need totally different * handling to run on them than a stand-alone application) */ -#define PLATFORM_NATIVE (1<<0) -#define PLATFORM_HOSTED (1<<1) -#define PLATFORM_ANDROID (1<<2) -#define PLATFORM_SDL (1<<3) -#define PLATFORM_CTRU (1<<4) +#define PLATFORM_NATIVE (1<<0) +#define PLATFORM_HOSTED (1<<1) +#define PLATFORM_ANDROID (1<<2) +#define PLATFORM_SDL (1<<3) +#define PLATFORM_GAME_CONSOLE (1<<4) /* CONFIG_KEYPAD */ #define IRIVER_H100_PAD 4 @@ -155,7 +155,7 @@ #define ECHO_R1_PAD 75 #define SURFANS_F28_PAD 76 #define RG_NANO_PAD 77 -#define CTRU_PAD 78 +#define N3DS_PAD 78 #define HIBY_R3PROII_PAD 79 #define HIDIZS_AP80MAX_PAD 80 @@ -575,8 +575,8 @@ Lyre prototype 1 */ #include "config/surfansf28.h" #elif defined(RG_NANO) #include "config/rgnano.h" -#elif defined(CTRU) -#include "config/ctru.h" +#elif defined(N3DS) +#include "config/3ds.h" #elif defined(HIBY_R3PROII) #include "config/hibyr3proii.h" #elif defined(HIBY_R1) @@ -1015,7 +1015,7 @@ Lyre prototype 1 */ #if defined(ASSEMBLER_THREADS) \ || defined(HAVE_WIN32_FIBER_THREADS) \ || defined(HAVE_SIGALTSTACK_THREADS) \ - || defined(CTRU) + || defined(N3DS) #define HAVE_PRIORITY_SCHEDULING #endif diff --git a/firmware/export/config/ctru.h b/firmware/export/config/3ds.h similarity index 94% rename from firmware/export/config/ctru.h rename to firmware/export/config/3ds.h index 414c99757c..302c671935 100644 --- a/firmware/export/config/ctru.h +++ b/firmware/export/config/3ds.h @@ -3,12 +3,12 @@ */ /* We don't run on hardware directly */ -#define CONFIG_PLATFORM (PLATFORM_HOSTED|PLATFORM_CTRU) +#define CONFIG_PLATFORM (PLATFORM_HOSTED|PLATFORM_GAME_CONSOLE) #define HAVE_FPU /* For Rolo and boot loader */ #define MODEL_NUMBER 100s -#define MODEL_NAME "CTRU" +#define MODEL_NAME "N3DS" #define USB_NONE @@ -73,9 +73,9 @@ /* #define HAVE_SCROLLWHEEL */ -#define CONFIG_KEYPAD CTRU_PAD +#define CONFIG_KEYPAD N3DS_PAD -#define HAVE_CTRU_AUDIO +#define HAVE_3DS_AUDIO #define HAVE_HEADPHONE_DETECTION #define HAVE_SW_TONE_CONTROLS /* #define HAVE_SW_VOLUME_CONTROL */ diff --git a/firmware/export/debug.h b/firmware/export/debug.h index 0cda5618bd..114f0b951a 100644 --- a/firmware/export/debug.h +++ b/firmware/export/debug.h @@ -34,8 +34,8 @@ extern void ldebugf(const char* file, int line, const char *fmt, ...) || (defined(APPLICATION) && defined(DEBUG)) #define DEBUGF debugf #define LDEBUGF(...) ldebugf(__FILE__, __LINE__, __VA_ARGS__) -#elif (CONFIG_PLATFORM & PLATFORM_CTRU) -/* let's use second display for debug output */ +#elif (CONFIG_PLATFORM & PLATFORM_GAME_CONSOLE) +/* always keep debugf available for console platforms */ #define DEBUGF debugf #define LDEBUGF(...) ldebugf(__FILE__, __LINE__, __VA_ARGS__) #elif defined(DEBUG) /* DEBUG on native targets */ diff --git a/firmware/export/rbpaths.h b/firmware/export/rbpaths.h index 08691eb0a3..de4a3bbb71 100644 --- a/firmware/export/rbpaths.h +++ b/firmware/export/rbpaths.h @@ -40,7 +40,7 @@ /* NOTE: target-specific hosted HOME_DIR resides in filesystem-app.c */ #if !defined(APPLICATION) || defined(SAMSUNG_YPR0) || defined(SAMSUNG_YPR1) || \ defined(DX50) || defined(DX90) || defined(SONY_NWZ_LINUX) || \ - defined(HIBY_LINUX) || defined(FIIO_M3K_LINUX) || defined(CTRU) + defined(HIBY_LINUX) || defined(FIIO_M3K_LINUX) || defined(N3DS) #define HOME_DIR "/" @@ -87,7 +87,7 @@ #if defined(APPLICATION) && \ !(defined(SAMSUNG_YPR0) || defined(SAMSUNG_YPR1) || \ defined(DX50) || defined(DX90) || defined(SONY_NWZ_LINUX) || \ - defined(HIBY_LINUX) || defined(FIIO_M3K_LINUX) || defined(CTRU)) + defined(HIBY_LINUX) || defined(FIIO_M3K_LINUX) || defined(N3DS)) #define PLUGIN_DATA_DIR ROCKBOX_DIR "/rocks.data" #define PLUGIN_GAMES_DATA_DIR PLUGIN_DATA_DIR diff --git a/firmware/include/dir.h b/firmware/include/dir.h index 9cc32f522c..844b56ac1d 100644 --- a/firmware/include/dir.h +++ b/firmware/include/dir.h @@ -27,8 +27,8 @@ #include "fs_attr.h" #include "fs_defines.h" -#if defined(CTRU) && !defined(SIMULATOR) -#include "filesystem-ctru.h" +#if (CONFIG_PLATFORM & PLATFORM_GAME_CONSOLE) && !defined(SIMULATOR) +#include "filesystem-console.h" #elif defined (APPLICATION) || defined(CHECKWPS) #include "filesystem-app.h" #elif defined(SIMULATOR) || defined(DBTOOL) diff --git a/firmware/include/file.h b/firmware/include/file.h index 72d8cbcafc..470550a2a7 100644 --- a/firmware/include/file.h +++ b/firmware/include/file.h @@ -39,8 +39,8 @@ enum relate_result RELATE_PREFIX, /* the path2 contains path1 as a prefix */ }; -#if defined(CTRU) && !defined(SIMULATOR) -#include "filesystem-ctru.h" +#if (CONFIG_PLATFORM & PLATFORM_GAME_CONSOLE) && !defined(SIMULATOR) +#include "filesystem-console.h" #elif defined(APPLICATION) || defined(CHECKWPS) #include "filesystem-app.h" #elif defined(SIMULATOR) || defined(DBTOOL) diff --git a/firmware/include/rbendian.h b/firmware/include/rbendian.h index 039d3f9521..97bdcd80ab 100644 --- a/firmware/include/rbendian.h +++ b/firmware/include/rbendian.h @@ -27,7 +27,7 @@ #endif #ifndef __MINGW32__ -#if defined(__APPLE__) || defined(CTRU) +#if defined(__APPLE__) || defined(N3DS) #include #else #include diff --git a/firmware/kernel/include/thread.h b/firmware/kernel/include/thread.h index f23c4d9572..286fd97026 100644 --- a/firmware/kernel/include/thread.h +++ b/firmware/kernel/include/thread.h @@ -88,7 +88,7 @@ struct thread_entry; * * simulator (possibly) doesn't simulate stack usage anyway but well ... */ -#if defined(HAVE_SDL_THREADS) || defined(__PCTOOL__) || defined(CTRU) +#if defined(HAVE_SDL_THREADS) || defined(__PCTOOL__) || defined(N3DS) #define DEFAULT_STACK_SIZE 0x100 /* tiny, ignored anyway */ #else #include "asm/thread.h" diff --git a/firmware/kernel/mutex.c b/firmware/kernel/mutex.c index 92e8594efe..8c8ddd224d 100644 --- a/firmware/kernel/mutex.c +++ b/firmware/kernel/mutex.c @@ -74,7 +74,7 @@ void mutex_lock(struct mutex *m) /* Release ownership of a mutex object - only owning thread must call this */ void mutex_unlock(struct mutex *m) { -#ifndef CTRU +#ifndef N3DS /* FIXME: synchronization primitives does not behave correctly between different cores */ /* unlocker not being the owner is an unlocking violation */ diff --git a/firmware/kernel/thread-common.c b/firmware/kernel/thread-common.c index 7fd071b026..25c49069f9 100644 --- a/firmware/kernel/thread-common.c +++ b/firmware/kernel/thread-common.c @@ -230,7 +230,7 @@ void format_thread_name(char *buf, size_t bufsize, snprintf(buf, bufsize, fmt, name, thread->id); } -#if !defined(HAVE_SDL_THREADS) && !defined(CTRU) +#if !defined(HAVE_SDL_THREADS) && !(CONFIG_PLATFORM & PLATFORM_GAME_CONSOLE) /*--------------------------------------------------------------------------- * Returns the maximum percentage of the stack ever used during runtime. *--------------------------------------------------------------------------- @@ -251,7 +251,7 @@ static unsigned int stack_usage(uintptr_t *stackptr, size_t stack_size) return usage; } -#endif /* !defined(HAVE_SDL_THREADS) && !defined(CTRU) */ +#endif /* !defined(HAVE_SDL_THREADS) && !(CONFIG_PLATFORM & PLATFORM_GAME_CONSOLE) */ #if NUM_CORES > 1 int core_get_debug_info(unsigned int core, struct core_debug_info *infop) @@ -303,7 +303,7 @@ int thread_get_debug_info(unsigned int thread_id, #ifdef HAVE_SCHEDULER_BOOSTCTRL cpu_boost = thread->cpu_boost; #endif -#if !defined(HAVE_SDL_THREADS) && !defined(CTRU) +#if !defined(HAVE_SDL_THREADS) && !(CONFIG_PLATFORM & PLATFORM_GAME_CONSOLE) infop->stack_usage = stack_usage(thread->stack, thread->stack_size); size_t stack_used_current = diff --git a/firmware/kernel/thread-internal.h b/firmware/kernel/thread-internal.h index 427e6dd033..fcf9bb56d9 100644 --- a/firmware/kernel/thread-internal.h +++ b/firmware/kernel/thread-internal.h @@ -31,7 +31,7 @@ * * simulator (possibly) doesn't simulate stack usage anyway but well ... */ -#if defined(HAVE_SDL_THREADS) || defined(__PCTOOL__) || defined(CTRU) +#if defined(HAVE_SDL_THREADS) || defined(__PCTOOL__) || defined(N3DS) struct regs { void *t; /* OS thread */ diff --git a/firmware/target/hosted/ctru/timer-ctru_sys.c b/firmware/target/hosted/ctru/timer-ctru_sys.c deleted file mode 100644 index 2b40fa1343..0000000000 --- a/firmware/target/hosted/ctru/timer-ctru_sys.c +++ /dev/null @@ -1,60 +0,0 @@ -/*************************************************************************** -* __________ __ ___. -* Open \______ \ ____ ____ | | _\_ |__ _______ ___ -* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / -* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < -* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ -* \/ \/ \/ \/ \/ -* -* Copyright (C) 2025 Mauricio G. -* -* This program is free software; you can redistribute it and/or -* modify it under the terms of the GNU General Public License -* as published by the Free Software Foundation; either version 2 -* of the License, or (at your option) any later version. -* -* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -* KIND, either express or implied. -* -****************************************************************************/ - -#include "sys_timer.h" -#include "timer.h" - -static int timer_prio = -1; -void (*global_timer_callback)(void); -int timerId; - -u32 _timer_callback(u32 interval, void *param){ - (void)param; - global_timer_callback(); - return(interval); -} - -#define cycles_to_miliseconds(cycles) \ - ((int)((1000*cycles)/TIMER_FREQ)) - -bool timer_register(int reg_prio, void (*unregister_callback)(void), - long cycles, void (*timer_callback)(void)) -{ - (void)unregister_callback; - if (reg_prio <= timer_prio || cycles == 0) - return false; - timer_prio=reg_prio; - global_timer_callback=timer_callback; - timerId=sys_add_timer(cycles_to_miliseconds(cycles), _timer_callback, 0); - return true; -} - -bool timer_set_period(long cycles) -{ - sys_remove_timer(timerId); - timerId=sys_add_timer(cycles_to_miliseconds(cycles), _timer_callback, 0); - return true; -} - -void timer_unregister(void) -{ - sys_remove_timer(timerId); - timer_prio = -1; -} diff --git a/firmware/target/hosted/filesystem-app.h b/firmware/target/hosted/filesystem-app.h index 423e723dd1..c526a1bdcf 100644 --- a/firmware/target/hosted/filesystem-app.h +++ b/firmware/target/hosted/filesystem-app.h @@ -57,11 +57,7 @@ const char * handle_special_dirs(const char *dir, unsigned flags, #ifdef WIN32 #include "filesystem-win32.h" #else /* !WIN32 */ -#if defined(CTRU) && !defined(SIMULATOR) -#include "filesystem-ctru.h" -#else #include "filesystem-unix.h" -#endif #endif /* WIN32 */ #include "filesystem-hosted.h" diff --git a/firmware/target/hosted/ctru/filesystem-ctru.h b/firmware/target/hosted/filesystem-console.h similarity index 55% rename from firmware/target/hosted/ctru/filesystem-ctru.h rename to firmware/target/hosted/filesystem-console.h index 086af83a67..ee10a725e5 100644 --- a/firmware/target/hosted/ctru/filesystem-ctru.h +++ b/firmware/target/hosted/filesystem-console.h @@ -17,8 +17,8 @@ * KIND, either express or implied. * ****************************************************************************/ -#ifndef _FILESYSTEM_CTRU_H_ -#define _FILESYSTEM_CTRU_H_ +#ifndef _FILESYSTEM_CONSOLE_H_ +#define _FILESYSTEM_CONSOLE_H_ #if defined(PLUGIN) || defined(CODEC) #define FILEFUNCTIONS_DECLARED @@ -29,17 +29,17 @@ #endif /* PLUGIN || CODEC */ #ifndef OSFUNCTIONS_DECLARED -#define FS_PREFIX(_x_) ctru_ ## _x_ +#define FS_PREFIX(_x_) console_ ## _x_ void paths_init(void); #endif /* !OSFUNCTIONS_DECLARED */ -#endif /* _FILESYSTEM_CTRU_H_ */ +#endif /* _FILESYSTEM_CONSOLE_H_ */ #ifdef _FILE_H_ #include -#ifndef _FILESYSTEM_CTRU__FILE_H_ -#define _FILESYSTEM_CTRU__FILE_H_ +#ifndef _FILESYSTEM_CONSOLE__FILE_H_ +#define _FILESYSTEM_CONSOLE__FILE_H_ #ifdef RB_FILESYSTEM_OS #define FILEFUNCTIONS_DEFINED @@ -52,31 +52,31 @@ void paths_init(void); #include -int ctru_open(const char *name, int oflag, ...); -int ctru_creat(const char *name, mode_t mode); -int ctru_close(int fildes); -int ctru_ftruncate(int fildes, off_t length); -int ctru_fsync(int fildes); -off_t ctru_lseek(int fildes, off_t offset, int whence); -ssize_t ctru_read(int fildes, void *buf, size_t nbyte); -ssize_t ctru_write(int fildes, const void *buf, size_t nbyte); -int ctru_remove(const char *path); -int ctru_rename(const char *old, const char *new); -int ctru_modtime(const char *path, time_t modtime); -off_t ctru_filesize(int fildes); -int ctru_fsamefile(int fildes1, int fildes2); -int ctru_relate(const char *path1, const char *path2); -bool ctru_file_exists(const char *path); -ssize_t ctru_readlink(const char *path, char *buf, size_t bufsiz); +int console_open(const char *name, int oflag, ...); +int console_creat(const char *name, mode_t mode); +int console_close(int fildes); +int console_ftruncate(int fildes, off_t length); +int console_fsync(int fildes); +off_t console_lseek(int fildes, off_t offset, int whence); +ssize_t console_read(int fildes, void *buf, size_t nbyte); +ssize_t console_write(int fildes, const void *buf, size_t nbyte); +int console_remove(const char *path); +int console_rename(const char *old, const char *new); +int console_modtime(const char *path, time_t modtime); +off_t console_filesize(int fildes); +int console_fsamefile(int fildes1, int fildes2); +int console_relate(const char *path1, const char *path2); +bool console_file_exists(const char *path); +ssize_t console_readlink(const char *path, char *buf, size_t bufsiz); #endif /* !FILEFUNCTIONS_DECLARED */ -#endif /* _FILESYSTEM_CTRU__FILE_H_ */ +#endif /* _FILESYSTEM_CONSOLE__FILE_H_ */ #endif /* _FILE_H_ */ #ifdef _DIR_H_ -#ifndef _FILESYSTEM_CTRU__DIR_H_ -#define _FILESYSTEM_CTRU__DIR_H_ +#ifndef _FILESYSTEM_CONSOLE__DIR_H_ +#define _FILESYSTEM_CONSOLE__DIR_H_ #define DIRENT dirent struct dirent; @@ -99,17 +99,17 @@ typedef struct {} DIR; #define DIRFUNCTIONS_DEFINED #endif -DIR * ctru_opendir(const char *dirname); -struct dirent * ctru_readdir(DIR *dirp); -int ctru_readdir_r(DIR *dirp, struct dirent *entry, +DIR * console_opendir(const char *dirname); +struct dirent * console_readdir(DIR *dirp); +int console_readdir_r(DIR *dirp, struct dirent *entry, struct dirent **result); -void ctru_rewinddir(DIR *dirp); -int ctru_closedir(DIR *dirp); -int ctru_mkdir(const char *path); -int ctru_rmdir(const char *path); -int ctru_samedir(DIR *dirp1, DIR *dirp2); -bool ctru_dir_exists(const char *dirname); +void console_rewinddir(DIR *dirp); +int console_closedir(DIR *dirp); +int console_mkdir(const char *path); +int console_rmdir(const char *path); +int console_samedir(DIR *dirp1, DIR *dirp2); +bool console_dir_exists(const char *dirname); #endif /* !DIRFUNCTIONS_DECLARED */ -#endif /* _FILESYSTEM_CTRU__DIR_H_ */ +#endif /* _FILESYSTEM_CONSOLE__DIR_H_ */ #endif /* _DIR_H_ */ diff --git a/firmware/target/hosted/ctru/backlight-ctru.c b/firmware/target/hosted/game_console/3ds/backlight-3ds.c similarity index 99% rename from firmware/target/hosted/ctru/backlight-ctru.c rename to firmware/target/hosted/game_console/3ds/backlight-3ds.c index f72ea46d1e..c171e5af56 100644 --- a/firmware/target/hosted/ctru/backlight-ctru.c +++ b/firmware/target/hosted/game_console/3ds/backlight-3ds.c @@ -34,7 +34,7 @@ #include "lcd.h" #include "debug.h" -#include "luminance-ctru.h" +#include "luminance-3ds.h" /* TODO: To use calibrated values in rockbox, MIN_BRIGHTNESS_SETTING (etc) would need to be diff --git a/firmware/target/hosted/ctru/button-ctru.c b/firmware/target/hosted/game_console/3ds/button-3ds.c similarity index 99% rename from firmware/target/hosted/ctru/button-ctru.c rename to firmware/target/hosted/game_console/3ds/button-3ds.c index 7c11025fb2..a7144b99e5 100644 --- a/firmware/target/hosted/ctru/button-ctru.c +++ b/firmware/target/hosted/game_console/3ds/button-3ds.c @@ -33,7 +33,7 @@ #include "kernel.h" #include "backlight.h" #include "system.h" -#include "button-ctru.h" +#include "button-console.h" #include "buttonmap.h" #include "debug.h" #include "powermgmt.h" diff --git a/firmware/target/hosted/ctru/filesystem-ctru.c b/firmware/target/hosted/game_console/3ds/filesystem-3ds.c similarity index 100% rename from firmware/target/hosted/ctru/filesystem-ctru.c rename to firmware/target/hosted/game_console/3ds/filesystem-3ds.c diff --git a/firmware/target/hosted/ctru/lc-ctru.c b/firmware/target/hosted/game_console/3ds/lc-3ds.c similarity index 90% rename from firmware/target/hosted/ctru/lc-ctru.c rename to firmware/target/hosted/game_console/3ds/lc-3ds.c index af7cfc8655..110e992804 100644 --- a/firmware/target/hosted/ctru/lc-ctru.c +++ b/firmware/target/hosted/game_console/3ds/lc-3ds.c @@ -29,20 +29,19 @@ #include "system.h" #include "load_code.h" -#include "filesystem-ctru.h" +#include "filesystem-console.h" #include "debug.h" -extern u32 __ctrl_code_allocator_pages; +/* reserve only the required memory for rockbox app, + the rest will be used to load .so objects */ +u32 __ctru_heap_size = MEMORYSIZE * 1024 * 1024; +u32 __ctru_linear_heap_size = MEMORYSIZE * 1024 * 1024; void* programResolver(const char* sym, void *userData); void * lc_open(const char *filename, unsigned char *buf, size_t buf_size) { DEBUGF("dlopen(path=\"%s\")\n", filename); - /* We need to increase __ctrl_code_allocator_pages value to 8 MB - to enable big plugins */ - __ctrl_code_allocator_pages = 2048; - /* note: the 3ds dlopen implementation needs a custom resolver for the unresolved symbols in shared objects */ /* void *handle = dlopen(filename, RTLD_NOW | RTLD_LOCAL); */ diff --git a/firmware/target/hosted/ctru/lc-program-resolver.c b/firmware/target/hosted/game_console/3ds/lc-program-resolver.c similarity index 100% rename from firmware/target/hosted/ctru/lc-program-resolver.c rename to firmware/target/hosted/game_console/3ds/lc-program-resolver.c diff --git a/firmware/target/hosted/ctru/lcd-bitmap.c b/firmware/target/hosted/game_console/3ds/lcd-bitmap-3ds.c similarity index 99% rename from firmware/target/hosted/ctru/lcd-bitmap.c rename to firmware/target/hosted/game_console/3ds/lcd-bitmap-3ds.c index 20e344e349..1d5b6bf1d5 100644 --- a/firmware/target/hosted/ctru/lcd-bitmap.c +++ b/firmware/target/hosted/game_console/3ds/lcd-bitmap-3ds.c @@ -33,7 +33,7 @@ #include "debug.h" #include "system.h" -#include "button-ctru.h" +#include "button-console.h" #include "screendump.h" #include "lcd-target.h" diff --git a/firmware/target/hosted/ctru/luminance-ctru.c b/firmware/target/hosted/game_console/3ds/luminance-3ds.c similarity index 99% rename from firmware/target/hosted/ctru/luminance-ctru.c rename to firmware/target/hosted/game_console/3ds/luminance-3ds.c index 7d1d0a3310..6e59686a2a 100644 --- a/firmware/target/hosted/ctru/luminance-ctru.c +++ b/firmware/target/hosted/game_console/3ds/luminance-3ds.c @@ -28,7 +28,7 @@ #include <3ds.h> #include -#include "luminance-ctru.h" +#include "luminance-3ds.h" // For accessing physmem uncached (and directly) #define PA_PTR(addr) (void *)((u32)(addr) | 1 << 31) diff --git a/firmware/target/hosted/ctru/luminance-ctru.h b/firmware/target/hosted/game_console/3ds/luminance-3ds.h similarity index 93% rename from firmware/target/hosted/ctru/luminance-ctru.h rename to firmware/target/hosted/game_console/3ds/luminance-3ds.h index 148935b790..c28117aa3e 100644 --- a/firmware/target/hosted/ctru/luminance-ctru.h +++ b/firmware/target/hosted/game_console/3ds/luminance-3ds.h @@ -24,8 +24,8 @@ * reasonable ways as different from the original version. */ -#ifndef _LUMINANCE_CTRU_H_ -#define _LUMINANCE_CTRU_H_ +#ifndef _LUMINANCE_CONSOLE_H_ +#define _LUMINANCE_CONSOLE_H_ #include <3ds/types.h> @@ -37,4 +37,4 @@ u32 getMinLuminancePreset(void); u32 getMaxLuminancePreset(void); u32 getCurrentLuminance(bool top); -#endif /* _LUMINANCE_CTRU_H_ */ +#endif /* _LUMINANCE_CONSOLE_H_ */ diff --git a/firmware/target/hosted/ctru/pcm-ctru.c b/firmware/target/hosted/game_console/3ds/pcm-3ds.c similarity index 97% rename from firmware/target/hosted/ctru/pcm-ctru.c rename to firmware/target/hosted/game_console/3ds/pcm-3ds.c index 999b87ca0e..01335febde 100644 --- a/firmware/target/hosted/ctru/pcm-ctru.c +++ b/firmware/target/hosted/game_console/3ds/pcm-3ds.c @@ -56,12 +56,6 @@ /*#define LOGF_ENABLE*/ #include "logf.h" -#ifdef DEBUG -extern bool debug_audio; -#endif - -extern const char *audiodev; - /* Bytes left in the Rockbox PCM frame buffer. */ static size_t _pcm_buffer_size = 0; @@ -207,7 +201,7 @@ static void sink_set_freq_nolock(uint16_t freq) mix[1] = 1.0; ndspChnSetMix(0, mix); - memset(_dsp_wave_bufs, 0, sizeof(_dsp_wave_bufs) * ARRAY_SIZE(_dsp_wave_bufs)); + memset(_dsp_wave_bufs, 0, sizeof(ndspWaveBuf) * ARRAY_SIZE(_dsp_wave_bufs)); const size_t wave_buffer_size = MIX_FRAME_SAMPLES * 2 * sizeof(s16); size_t buffer_size = wave_buffer_size * ARRAY_SIZE(_dsp_wave_bufs); @@ -233,7 +227,7 @@ static void sink_set_freq_nolock(uint16_t freq) NULL, 32 * 1024, /* 32kB stack size */ 0x18, /* high priority */ - -1, /* run on any core */ false); + -1, /* run on any core */ false); } static void sink_dma_start(const void *addr, size_t size) diff --git a/firmware/target/hosted/ctru/powermgmt-ctru.c b/firmware/target/hosted/game_console/3ds/powermgmt-3ds.c similarity index 100% rename from firmware/target/hosted/ctru/powermgmt-ctru.c rename to firmware/target/hosted/game_console/3ds/powermgmt-3ds.c diff --git a/firmware/target/hosted/ctru/system-ctru.c b/firmware/target/hosted/game_console/3ds/system-3ds.c similarity index 97% rename from firmware/target/hosted/ctru/system-ctru.c rename to firmware/target/hosted/game_console/3ds/system-3ds.c index 0a9fb1b2ec..473a91c699 100644 --- a/firmware/target/hosted/ctru/system-ctru.c +++ b/firmware/target/hosted/game_console/3ds/system-3ds.c @@ -32,9 +32,9 @@ #include "system.h" #include "kernel.h" -#include "thread-ctru.h" -#include "system-ctru.h" -#include "button-ctru.h" +#include "thread-console.h" +#include "system-console.h" +#include "button-console.h" #include "lcd-bitmap.h" #include "panic.h" #include "debug.h" diff --git a/firmware/target/hosted/ctru/app/adc-target.h b/firmware/target/hosted/game_console/adc-target.h similarity index 100% rename from firmware/target/hosted/ctru/app/adc-target.h rename to firmware/target/hosted/game_console/adc-target.h diff --git a/firmware/target/hosted/ctru/app/backlight-target.h b/firmware/target/hosted/game_console/backlight-target.h similarity index 100% rename from firmware/target/hosted/ctru/app/backlight-target.h rename to firmware/target/hosted/game_console/backlight-target.h diff --git a/firmware/target/hosted/ctru/button-ctru.h b/firmware/target/hosted/game_console/button-console.h similarity index 92% rename from firmware/target/hosted/ctru/button-ctru.h rename to firmware/target/hosted/game_console/button-console.h index 606c7d09f4..2cfea0cfea 100644 --- a/firmware/target/hosted/ctru/button-ctru.h +++ b/firmware/target/hosted/game_console/button-console.h @@ -19,8 +19,8 @@ ****************************************************************************/ -#ifndef __BUTTON_CTRU_H__ -#define __BUTTON_CTRU_H__ +#ifndef __BUTTON_CONSOLE_H__ +#define __BUTTON_CONSOLE_H__ #include #include "config.h" @@ -30,4 +30,4 @@ bool button_hold(void); void button_init_device(void); int button_read_device(int *data); -#endif /* __BUTTON_CTRU_H__ */ +#endif /* __BUTTON_CONSOLE_H__ */ diff --git a/firmware/target/hosted/ctru/app/button-target.h b/firmware/target/hosted/game_console/button-target.h similarity index 100% rename from firmware/target/hosted/ctru/app/button-target.h rename to firmware/target/hosted/game_console/button-target.h diff --git a/firmware/target/hosted/ctru/buttonmap.h b/firmware/target/hosted/game_console/buttonmap.h similarity index 100% rename from firmware/target/hosted/ctru/buttonmap.h rename to firmware/target/hosted/game_console/buttonmap.h diff --git a/firmware/target/hosted/ctru/kernel-ctru.c b/firmware/target/hosted/game_console/kernel-console.c similarity index 87% rename from firmware/target/hosted/ctru/kernel-ctru.c rename to firmware/target/hosted/game_console/kernel-console.c index 27d7f2cee0..f68b807a71 100644 --- a/firmware/target/hosted/ctru/kernel-ctru.c +++ b/firmware/target/hosted/game_console/kernel-console.c @@ -5,8 +5,9 @@ * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ + * $Id$ * - * Copyright (C) 2025 Mauricio G. + * Copyright (C) 2002 by Felix Arends * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -21,13 +22,16 @@ #include #include #include -#include "system-ctru.h" -#include "thread-ctru.h" +#include "system-console.h" +#include "thread-console.h" #include "kernel.h" #include "thread.h" #include "panic.h" #include "debug.h" +#include "sys_thread.h" +#include "sys_timer.h" + static int tick_timer_id; long start_tick; @@ -35,7 +39,7 @@ long start_tick; static sysCond *sim_thread_cond; /* Mutex to serialize changing levels and exclude other threads while * inside a handler */ -static RecursiveLock sim_irq_mtx; +static sysMutex *sim_irq_mtx; /* Level: 0 = enabled, not 0 = disabled */ static int volatile interrupt_level = HIGHEST_IRQ_LEVEL; /* How many handers waiting? Not strictly needed because CondSignal is a @@ -54,7 +58,7 @@ static int status_reg = 0; */ int set_irq_level(int level) { - RecursiveLock_Lock(&sim_irq_mtx); + sys_mutex_lock(sim_irq_mtx); int oldlevel = interrupt_level; @@ -68,13 +72,13 @@ int set_irq_level(int level) interrupt_level = level; /* save new level */ - RecursiveLock_Unlock(&sim_irq_mtx); + sys_mutex_unlock(sim_irq_mtx); return oldlevel; } void sim_enter_irq_handler(void) { - RecursiveLock_Lock(&sim_irq_mtx); + sys_mutex_lock(sim_irq_mtx); handlers_pending++; /* Check each time before proceeding: disabled->enabled->...->disabled @@ -82,7 +86,7 @@ void sim_enter_irq_handler(void) * the mutex; a handler can also leave "interrupts" disabled during * its execution */ while (interrupt_level != 0) - sys_cond_wait(sim_thread_cond, &sim_irq_mtx); + sys_cond_wait(sim_thread_cond, sim_irq_mtx); status_reg = 1; } @@ -94,12 +98,18 @@ void sim_exit_irq_handler(void) sys_cond_signal(sim_thread_cond); status_reg = 0; - RecursiveLock_Unlock(&sim_irq_mtx); + sys_mutex_unlock(sim_irq_mtx); } static bool sim_kernel_init(void) { - RecursiveLock_Init(&sim_irq_mtx); + sim_irq_mtx = (sysMutex *) sys_mutex_create(); + if (sim_irq_mtx == NULL) + { + panicf("Cannot create sim_irq_mtx\n"); + return false; + } + sim_thread_cond = sys_cond_create(); if (sim_thread_cond == NULL) { @@ -116,7 +126,8 @@ void sim_kernel_shutdown(void) while(handlers_pending > 0) sys_delay(10); - sys_cond_destroy(sim_thread_cond); + free(sim_irq_mtx); + free(sim_thread_cond); } u32 tick_timer(u32 interval, void *param) @@ -161,5 +172,4 @@ void tick_start(unsigned int interval_in_ms) } tick_timer_id = sys_add_timer(interval_in_ms, tick_timer, NULL); -} - +} \ No newline at end of file diff --git a/firmware/target/hosted/ctru/lcd-bitmap.h b/firmware/target/hosted/game_console/lcd-bitmap.h similarity index 100% rename from firmware/target/hosted/ctru/lcd-bitmap.h rename to firmware/target/hosted/game_console/lcd-bitmap.h diff --git a/firmware/target/hosted/ctru/lcd-target.h b/firmware/target/hosted/game_console/lcd-target.h similarity index 100% rename from firmware/target/hosted/ctru/lcd-target.h rename to firmware/target/hosted/game_console/lcd-target.h diff --git a/firmware/target/hosted/ctru/lib/bfile/bfile.c b/firmware/target/hosted/game_console/lib/bfile/bfile-3ds.c similarity index 84% rename from firmware/target/hosted/ctru/lib/bfile/bfile.c rename to firmware/target/hosted/game_console/lib/bfile/bfile-3ds.c index e7a8625e71..2aa5f53f64 100644 --- a/firmware/target/hosted/ctru/lib/bfile/bfile.c +++ b/firmware/target/hosted/game_console/lib/bfile/bfile-3ds.c @@ -12,59 +12,6 @@ #endif #include "logf.h" -// rw mutex implementation -void sync_RWMutexInit(sync_RWMutex *m) { - LightLock_Init(&m->shared); - CondVar_Init(&m->reader_q); - CondVar_Init(&m->writer_q); - m->active_readers = 0; - m->active_writers = 0; - m->waiting_writers = 0; -} - -static inline LightLock *unique_lock(LightLock *lk) { - LightLock_Lock(lk); - return lk; -} - -void sync_RWMutexRLock(sync_RWMutex *m) { - LightLock *lk = unique_lock(&m->shared); - while (m->waiting_writers != 0) { - CondVar_Wait(&m->reader_q, lk); - } - ++m->active_readers; - LightLock_Unlock(lk); -} - -void sync_RWMutexRUnlock(sync_RWMutex *m) { - LightLock *lk = unique_lock(&m->shared); - --m->active_readers; - LightLock_Unlock(lk); - CondVar_Signal(&m->writer_q); -} - -void sync_RWMutexLock(sync_RWMutex *m) { - LightLock *lk = unique_lock(&m->shared); - ++m->waiting_writers; - while ((m->active_readers != 0) || (m->active_writers != 0)) { - CondVar_Wait(&m->writer_q, lk); - } - ++m->active_writers; - LightLock_Unlock(lk); -} - -void sync_RWMutexUnlock(sync_RWMutex *m) { - LightLock *lk = unique_lock(&m->shared); - --m->waiting_writers; - --m->active_writers; - if (m->waiting_writers > 0) { - CondVar_Signal(&m->writer_q); - } else { - CondVar_Broadcast(&m->reader_q); - } - LightLock_Unlock(lk); -} - // page reader implementation file_error_t readPage(PageReader *p, u64 offset) { diff --git a/firmware/target/hosted/ctru/lib/bfile/bfile-internal.h b/firmware/target/hosted/game_console/lib/bfile/bfile-internal.h similarity index 74% rename from firmware/target/hosted/ctru/lib/bfile/bfile-internal.h rename to firmware/target/hosted/game_console/lib/bfile/bfile-internal.h index 62977a68ad..745dfc4a87 100644 --- a/firmware/target/hosted/ctru/lib/bfile/bfile-internal.h +++ b/firmware/target/hosted/game_console/lib/bfile/bfile-internal.h @@ -9,17 +9,7 @@ #include #include -#include <3ds/gfx.h> -#ifdef RGB565 -#undef RGB565 -#endif - -#include <3ds/svc.h> -#include <3ds/types.h> -#include <3ds/thread.h> -#include <3ds/result.h> -#include <3ds/services/fs.h> -#include <3ds/synchronization.h> +#include "sys_thread.h" /* #define MALLOC_DEBUG #include "rmalloc/rmalloc.h" */ @@ -32,29 +22,29 @@ typedef struct { \ } type_name##_t; /* single mutex implementation */ -#define sync_Mutex LightLock +#define sync_Mutex sysMutex static inline void sync_MutexInit(sync_Mutex* m) { - LightLock_Init(m); + sys_mutex_init(m); } /* mutex unlock */ static inline void sync_MutexLock(sync_Mutex* m) { - LightLock_Lock(m); + sys_mutex_lock(m); } /* mutex lock */ static inline void sync_MutexUnlock(sync_Mutex* m) { - LightLock_Unlock(m); + sys_mutex_unlock(m); } /* read_write mutex implementation */ typedef struct { - sync_Mutex shared; - CondVar reader_q; - CondVar writer_q; - int active_readers; - int active_writers; - int waiting_writers; + sync_Mutex shared; + sysCond reader_q; + sysCond writer_q; + int active_readers; + int active_writers; + int waiting_writers; } sync_RWMutex; void sync_RWMutexInit(sync_RWMutex *m); @@ -68,6 +58,7 @@ two_type_value(int, const char*, n, err, int_error); two_type_value(struct stat, const char*, fi, err, stat_error); typedef const char* file_error_t; +#ifdef N3DS typedef struct { Handle file; s64 start; @@ -77,5 +68,6 @@ typedef struct { u8 *buffer; } PageReader; +#endif #endif /* _BFILE_INTERNAL_H_ */ diff --git a/firmware/target/hosted/game_console/lib/bfile/bfile-rwmutex.c b/firmware/target/hosted/game_console/lib/bfile/bfile-rwmutex.c new file mode 100644 index 0000000000..d7ff6a2807 --- /dev/null +++ b/firmware/target/hosted/game_console/lib/bfile/bfile-rwmutex.c @@ -0,0 +1,61 @@ + +#include "bfile.h" + +/* Define LOGF_ENABLE to enable logf output in this file */ +#if 0 +#define LOGF_ENABLE +#endif +#include "logf.h" + +/* rw mutex implementation */ +void sync_RWMutexInit(sync_RWMutex *m) { + sys_mutex_init(&m->shared); + sys_cond_init(&m->reader_q); + sys_cond_init(&m->writer_q); + m->active_readers = 0; + m->active_writers = 0; + m->waiting_writers = 0; +} + +static inline sysMutex *unique_lock(sysMutex *lk) { + sys_mutex_lock(lk); + return lk; +} + +void sync_RWMutexRLock(sync_RWMutex *m) { + sysMutex *lk = unique_lock(&m->shared); + while (m->waiting_writers != 0) { + sys_cond_wait(&m->reader_q, lk); + } + ++m->active_readers; + sys_mutex_unlock(lk); +} + +void sync_RWMutexRUnlock(sync_RWMutex *m) { + sysMutex *lk = unique_lock(&m->shared); + --m->active_readers; + sys_mutex_unlock(lk); + sys_cond_signal(&m->writer_q); +} + +void sync_RWMutexLock(sync_RWMutex *m) { + sysMutex *lk = unique_lock(&m->shared); + ++m->waiting_writers; + while ((m->active_readers != 0) || (m->active_writers != 0)) { + sys_cond_wait(&m->writer_q, lk); + } + ++m->active_writers; + sys_mutex_unlock(lk); +} + +void sync_RWMutexUnlock(sync_RWMutex *m) { + sysMutex *lk = unique_lock(&m->shared); + --m->waiting_writers; + --m->active_writers; + if (m->waiting_writers > 0) { + sys_cond_signal(&m->writer_q); + } else { + sys_cond_broadcast(&m->reader_q); + } + sys_mutex_unlock(lk); +} \ No newline at end of file diff --git a/firmware/target/hosted/ctru/lib/bfile/bfile.h b/firmware/target/hosted/game_console/lib/bfile/bfile.h similarity index 100% rename from firmware/target/hosted/ctru/lib/bfile/bfile.h rename to firmware/target/hosted/game_console/lib/bfile/bfile.h diff --git a/firmware/target/hosted/game_console/lib/sys_atomic.c b/firmware/target/hosted/game_console/lib/sys_atomic.c new file mode 100644 index 0000000000..22ad664ba1 --- /dev/null +++ b/firmware/target/hosted/game_console/lib/sys_atomic.c @@ -0,0 +1,60 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2026 Mauricio G. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include "debug.h" +#include "logf.h" + +#include "sys_atomic.h" + +bool _AtomicCAS(u32 *ptr, int oldval, int newval) +{ + return (bool) __sync_bool_compare_and_swap(ptr, oldval, newval); +} + +bool _AtomicTryLock(int *lock) +{ + return __sync_lock_test_and_set(lock, 1) == 0; +} + +void sys_delay(u32 ms); +#define CPUPauseInstruction() +void AtomicLock(int *lock) +{ + int iterations = 0; + while (!_AtomicTryLock(lock)) { + if (iterations < 32) { + iterations++; + CPUPauseInstruction(); + } else { + sys_delay(0); + } + } +} + +void AtomicUnlock(int *lock) +{ + __sync_lock_release(lock); +} diff --git a/firmware/target/hosted/game_console/lib/sys_atomic.h b/firmware/target/hosted/game_console/lib/sys_atomic.h new file mode 100644 index 0000000000..722eff2a77 --- /dev/null +++ b/firmware/target/hosted/game_console/lib/sys_atomic.h @@ -0,0 +1,37 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2026 Mauricio G. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef __SYSATOMIC_H__ +#define __SYSATOMIC_H__ + +#include "sys_types.h" + +#define AtomicAdd(ptr, value) __sync_fetch_and_add((u32 *)(ptr), value) + +bool _AtomicCAS(u32 *ptr, int oldval, int newval); +#define AtomicGet(ptr) __atomic_load_n((u32*)(ptr), __ATOMIC_SEQ_CST) +#define AtomicSet(ptr, value) __sync_lock_test_and_set((u32 *)(ptr), value) +#define AtomicCAS(ptr, oldvalue, newvalue) _AtomicCAS((u32 *)(ptr), oldvalue, newvalue) +void AtomicLock(int *lock); +void AtomicUnlock(int *lock); + +#endif /* #ifndef __SYSATOMIC_H__ */ + diff --git a/firmware/target/hosted/game_console/lib/sys_cond_generic.c b/firmware/target/hosted/game_console/lib/sys_cond_generic.c new file mode 100644 index 0000000000..15b58c1505 --- /dev/null +++ b/firmware/target/hosted/game_console/lib/sys_cond_generic.c @@ -0,0 +1,154 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025 Mauricio Ga. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include "sys_thread.h" +#include "sys_timer.h" +#include "debug.h" +#include "logf.h" + +/* sysCond */ +void sys_cond_init(sysCond *cond) +{ + if (cond) { + memset(cond, 0, sizeof(sysCond)); + sys_mutex_init(&cond->lock); + sys_sem_init(&cond->wait_sem, 0); + sys_sem_init(&cond->wait_done, 0); + cond->waiting = cond->signals = 0; + } +} + +sysCond *sys_cond_create(void) +{ + sysCond *cond; + + cond = (sysCond *) malloc(sizeof(sysCond)); + if (cond) { + sys_cond_init(cond); + } else { + DEBUGF("sys_cond_create: out of memory.\n"); + } + return cond; +} + +/* Destroy a condition variable */ +void sys_cond_destroy(sysCond *cond) +{ + if (cond) { + free(cond); + } +} + +/* Restart one of the threads that are waiting on the condition variable */ +int sys_cond_signal(sysCond *cond) +{ + if (!cond) { + DEBUGF("sys_cond_signal: Invalid param 'cond'\n"); + return -1; + } + + /* If there are waiting threads not already signalled, then + signal the condition and wait for the thread to respond. + */ + sys_mutex_lock(&cond->lock); + if (cond->waiting > cond->signals) { + ++cond->signals; + sys_sem_post(&cond->wait_sem); + sys_mutex_unlock(&cond->lock); + sys_sem_wait(&cond->wait_done); + } else { + sys_mutex_unlock(&cond->lock); + } + + return 0; +} + +/* Restart all threads that are waiting on the condition variable */ +int sys_cond_broadcast(sysCond *cond) +{ + if (!cond) { + DEBUGF("sys_cond_signal: Invalid param 'cond'\n"); + return -1; + } + + /* If there are waiting threads not already signalled, then + signal the condition and wait for the thread to respond. + */ + sys_mutex_lock(&cond->lock); + if (cond->waiting > cond->signals) { + int i, num_waiting; + + num_waiting = (cond->waiting - cond->signals); + cond->signals = cond->waiting; + for (i = 0; i < num_waiting; ++i) { + sys_sem_post(&cond->wait_sem); + } + /* Now all released threads are blocked here, waiting for us. + Collect them all (and win fabulous prizes!) :-) + */ + sys_mutex_unlock(&cond->lock); + for (i = 0; i < num_waiting; ++i) { + sys_sem_wait(&cond->wait_done); + } + } else { + sys_mutex_unlock(&cond->lock); + } + + return 0; +} + +int sys_cond_wait(sysCond *cond, sysMutex *mutex) +{ + if (!cond) { + DEBUGF("sys_cond_signal: Invalid param 'cond'\n"); + return -1; + } + + sys_mutex_lock(&cond->lock); + ++cond->waiting; + sys_mutex_unlock(&cond->lock); + + /* Unlock the mutex, as is required by condition variable semantics */ + sys_mutex_unlock(mutex); + + /* Wait for a signal */ + int retval = sys_sem_wait(&cond->wait_sem); + + sys_mutex_lock(&cond->lock); + if (cond->signals > 0) { + /* We always notify the signal thread that we are done */ + sys_sem_post(&cond->wait_done); + + /* Signal handshake complete */ + --cond->signals; + } + --cond->waiting; + sys_mutex_unlock(&cond->lock); + + /* Lock the mutex, as is required by condition variable semantics */ + sys_mutex_lock(mutex); + + return retval; +} diff --git a/firmware/target/hosted/ctru/lib/sys_dir.c b/firmware/target/hosted/game_console/lib/sys_dir_3ds.c similarity index 91% rename from firmware/target/hosted/ctru/lib/sys_dir.c rename to firmware/target/hosted/game_console/lib/sys_dir_3ds.c index 1088110b94..91a441987f 100644 --- a/firmware/target/hosted/ctru/lib/sys_dir.c +++ b/firmware/target/hosted/game_console/lib/sys_dir_3ds.c @@ -26,6 +26,7 @@ #include <3ds/util/utf.h> /* this includes a couple of 3ds headers */ #include "sys_file.h" +#include "sys_file_internal.h" #include "config.h" #include "debug.h" @@ -46,7 +47,7 @@ static struct dirstr_desc { struct filestr_base stream; /* basic stream info (first!) */ struct dirent entry; /* current parsed entry information */ -} open_streams[MAX_OPEN_DIRS] = +} open_dir_streams[MAX_OPEN_DIRS] = { [0 ... MAX_OPEN_FILES-1] = { .stream = { .cache = NULL } } }; @@ -58,7 +59,7 @@ static struct dirstr_desc * get_dirstr(DIR *dirp) { struct dirstr_desc *dir = (struct dirstr_desc *)dirp; - if (!PTR_IN_ARRAY(open_streams, dir, MAX_OPEN_DIRS)) + if (!PTR_IN_ARRAY(open_dir_streams, dir, MAX_OPEN_DIRS)) dir = NULL; else if (dir->stream.cache != NULL) return dir; @@ -71,7 +72,7 @@ static struct dirstr_desc * get_dirstr(DIR *dirp) } else { - logf("dir #%d: dir not open\n", (int)(dir - open_streams)); + logf("dir #%d: dir not open\n", (int)(dir - open_dir_streams)); errnum = EBADF; } @@ -104,7 +105,7 @@ static struct dirstr_desc * alloc_dirstr(void) { for (unsigned int dd = 0; dd < MAX_OPEN_DIRS; dd++) { - struct dirstr_desc *dir = &open_streams[dd]; + struct dirstr_desc *dir = &open_dir_streams[dd]; if (dir->stream.cache == NULL) return dir; } @@ -119,19 +120,10 @@ u32 fs_error(void) { return err; } -/* Initialize the base descriptor */ -static void filestr_base_init(struct filestr_base *stream) -{ - stream->cache = NULL; - stream->handle = 0; - stream->size = 0; - LightLock_Init(&stream->mtx); -} - /** POSIX interface **/ /* open a directory */ -DIR * ctru_opendir(const char *dirname) +DIR * console_opendir(const char *dirname) { logf("opendir(dirname=\"%s\")\n", dirname); @@ -161,7 +153,7 @@ DIR * ctru_opendir(const char *dirname) dir->stream.size = 0; dir->stream.flags = 0; - /* we will use file path to implement ctru_samedir function */ + /* we will use file path to implement console_samedir function */ strcpy(dir->stream.path, dirname); dirp = (DIR *)dir; @@ -175,7 +167,7 @@ file_error: } /* close a directory stream */ -int ctru_closedir(DIR *dirp) +int console_closedir(DIR *dirp) { int rc; @@ -183,14 +175,14 @@ int ctru_closedir(DIR *dirp) /* needs to work even if marked "nonexistant" */ struct dirstr_desc * const dir = (struct dirstr_desc *)dirp; - if (!PTR_IN_ARRAY(open_streams, dir, MAX_OPEN_DIRS)) + if (!PTR_IN_ARRAY(open_dir_streams, dir, MAX_OPEN_DIRS)) FILE_ERROR(EFAULT, -1); logf("closedir(dirname=\"%s\")\n", dir->stream.path); if (dir->stream.cache == NULL) { - logf("dir #%d: dir not open\n", (int)(dir - open_streams)); + logf("dir #%d: dir not open\n", (int)(dir - open_dir_streams)); FILE_ERROR(EBADF, -2); } @@ -236,7 +228,7 @@ void dirstr_entry_init(FS_DirectoryEntry *dirEntry, struct dirent *entry) } /* read a directory */ -struct dirent * ctru_readdir(DIR *dirp) +struct dirent * console_readdir(DIR *dirp) { struct dirstr_desc * const dir = GET_DIRSTR(READER, dirp); if (!dir) @@ -294,7 +286,7 @@ enum }; /* make a directory */ -int ctru_mkdir(const char *path) +int console_mkdir(const char *path) { logf("mkdir(path=\"%s\")\n", path); @@ -321,7 +313,7 @@ file_error: } /* remove a directory */ -int ctru_rmdir(const char *name) +int console_rmdir(const char *name) { logf("rmdir(name=\"%s\")\n", name); @@ -355,7 +347,7 @@ file_error: /** Extended interface **/ /* return if two directory streams refer to the same directory */ -int ctru_samedir(DIR *dirp1, DIR *dirp2) +int console_samedir(DIR *dirp1, DIR *dirp2) { struct dirstr_desc * const dir1 = GET_DIRSTR(WRITER, dirp1); if (!dir1) @@ -373,7 +365,7 @@ int ctru_samedir(DIR *dirp1, DIR *dirp2) } /* test directory existence (returns 'false' if a file) */ -bool ctru_dir_exists(const char *dirname) +bool console_dir_exists(const char *dirname) { file_internal_lock_WRITER(); @@ -421,7 +413,7 @@ file_error: return (struct dirinfo){ .attribute = 0 }; } -const char* ctru_root_realpath(void) +const char* console_root_realpath(void) { /* Native only, for APP and SIM see respective filesystem-.c files */ return PATH_ROOTSTR; /* rb_namespace.c */ diff --git a/firmware/target/hosted/ctru/lib/sys_file.h b/firmware/target/hosted/game_console/lib/sys_file.h similarity index 87% rename from firmware/target/hosted/ctru/lib/sys_file.h rename to firmware/target/hosted/game_console/lib/sys_file.h index 4640b16db4..925b014716 100644 --- a/firmware/target/hosted/ctru/lib/sys_file.h +++ b/firmware/target/hosted/game_console/lib/sys_file.h @@ -27,27 +27,24 @@ #define strlcpy_from_os strlcpy -#define AtomicLoad(ptr) __atomic_load_n((u32*)(ptr), __ATOMIC_SEQ_CST) -#define AtomicAdd(ptr, value) __atomic_add_fetch((u32*)(ptr), value, __ATOMIC_SEQ_CST) - struct filestr_base { - PageReader* cache; /* buffer IO implementation (cache) */ - Handle handle; /* file handle */ - u64 size; /* file size */ - int flags; /* stream flags */ - char path[MAX_PATH+1]; + PageReader* cache; /* buffer IO implementation (cache) */ + sys_handle_type handle; /* file handle */ + u64 size; /* file size */ + int flags; /* stream flags */ + char path[MAX_PATH+1]; - LightLock mtx; /* serialization for this stream */ + sysMutex mtx; /* serialization for this stream */ }; static inline void filestr_lock(struct filestr_base *stream) { - LightLock_Lock(&stream->mtx); + sys_mutex_lock(&stream->mtx); } static inline void filestr_unlock(struct filestr_base *stream) { - LightLock_Unlock(&stream->mtx); + sys_mutex_unlock(&stream->mtx); } /* stream lock doesn't have to be used if getting RW lock writer access */ diff --git a/firmware/target/hosted/ctru/lib/sys_file.c b/firmware/target/hosted/game_console/lib/sys_file_3ds.c similarity index 50% rename from firmware/target/hosted/ctru/lib/sys_file.c rename to firmware/target/hosted/game_console/lib/sys_file_3ds.c index 72ebfa8fcd..924572ef0d 100644 --- a/firmware/target/hosted/ctru/lib/sys_file.c +++ b/firmware/target/hosted/game_console/lib/sys_file_3ds.c @@ -26,6 +26,7 @@ /* this includes a couple of 3ds headers */ #include "sys_file.h" +#include "sys_file_internal.h" #define RB_FILESYSTEM_OS #include "config.h" @@ -40,77 +41,8 @@ // #define LOGF_ENABLE #include "logf.h" -/** - * These functions provide a roughly POSIX-compatible file I/O API. - * Important: the bufferio I/O library (bfile) used in the 3ds does not work - * with write-only files due to an internal limitation. - * So all files will be opened with the read flag by default. - */ - -/* structure used for open file descriptors */ -static struct filestr_desc -{ - struct filestr_base stream; /* basic stream info (first!) */ - file_size_t offset; /* current offset for stream */ - u64 *sizep; /* shortcut to file size in fileobj */ -} open_streams[MAX_OPEN_FILES] = -{ - [0 ... MAX_OPEN_FILES-1] = { .stream = { .cache = NULL, .flags = 0 } } -}; - extern FS_Archive sdmcArchive; -/* check and return a struct filestr_desc* from a file descriptor number */ -static struct filestr_desc * get_filestr(int fildes) -{ - struct filestr_desc *file = &open_streams[fildes]; - - if ((unsigned int)fildes >= MAX_OPEN_FILES) - file = NULL; - else if (file->stream.cache != NULL) - return file; - - logf("fildes %d: bad file number\n", fildes); - errno = (file && (file->stream.cache == NULL)) ? ENXIO : EBADF; - return NULL; -} - -#define GET_FILESTR(type, fildes) \ - ({ \ - file_internal_lock_##type(); \ - struct filestr_desc * _file = get_filestr(fildes); \ - if (_file) \ - FILESTR_LOCK(type, &_file->stream); \ - else { \ - file_internal_unlock_##type(); \ - }\ - _file; \ - }) - -/* release the lock on the filestr_desc* */ -#define RELEASE_FILESTR(type, file) \ - ({ \ - FILESTR_UNLOCK(type, &(file)->stream); \ - file_internal_unlock_##type(); \ - }) - -/* find a free file descriptor */ -static int alloc_filestr(struct filestr_desc **filep) -{ - for (int fildes = 0; fildes < MAX_OPEN_FILES; fildes++) - { - struct filestr_desc *file = &open_streams[fildes]; - if (file->stream.cache == NULL) - { - *filep = file; - return fildes; - } - } - - logf("Too many files open\n"); - return -1; -} - /* check for file existence */ int test_stream_exists_internal(const char *path) { @@ -148,146 +80,6 @@ file_error: return rc; } -/* set the file pointer */ -static off_t lseek_internal(struct filestr_desc *file, off_t offset, - int whence) -{ - off_t rc; - off_t pos; - - off_t size = MIN(*file->sizep, FILE_SIZE_MAX); - off_t file_offset = AtomicLoad(&file->offset); - - switch (whence) - { - case SEEK_SET: - if (offset < 0 || (off_t)offset > size) - FILE_ERROR(EINVAL, -1); - - pos = offset; - break; - - case SEEK_CUR: - if ((offset < 0 && (off_t)-offset > file_offset) || - (offset > 0 && (off_t)offset > size - file_offset)) - FILE_ERROR(EINVAL, -1); - - pos = file_offset + offset; - break; - - case SEEK_END: - if (offset > 0 || (off_t)-offset > size) - FILE_ERROR(EINVAL, -1); - - pos = size + offset; - break; - - default: - FILE_ERROR(EINVAL, -1); - } - - AtomicSwap(&file->offset, pos); - - return pos; -file_error: - return rc; -} - -/* read from or write to the file; back end to read() and write() */ -static ssize_t readwrite(struct filestr_desc *file, void *buf, size_t nbyte, - bool write) -{ -#ifndef LOGF_ENABLE /* wipes out log before you can save it */ - /* DEBUGF("readwrite(%p,%lx,%lu,%s)\n", - file, (long)buf, (unsigned long)nbyte, write ? "write" : "read"); */ -#endif - - const file_size_t size = *file->sizep; - size_t filerem; - - if (write) - { - /* if opened in append mode, move pointer to end */ - if (file->stream.flags & O_APPEND) - AtomicSwap(&file->offset, MIN(size, FILE_SIZE_MAX)); - - filerem = FILE_SIZE_MAX - AtomicLoad(&file->offset); - } - else - { - /* limit to maximum possible offset (EOF or FILE_SIZE_MAX) */ - filerem = MIN(size, FILE_SIZE_MAX) - AtomicLoad(&file->offset); - } - - if (nbyte > filerem) - { - nbyte = filerem; - if (nbyte > 0) - {} - else if (write) - FILE_ERROR_RETURN(EFBIG, -1); /* would get too large */ - else if (AtomicLoad(&file->offset) >= FILE_SIZE_MAX) - FILE_ERROR_RETURN(EOVERFLOW, -2); /* can't read here */ - } - - if (nbyte == 0) - return 0; - - int rc = 0; - int_error_t n_err; - - if (write) { - n_err = PageReader_WriteAt(file->stream.cache, - buf, - nbyte, - AtomicLoad(&file->offset), - file->stream.flags & O_RDWR ? true : false); - } - else { - n_err = PageReader_ReadAt(file->stream.cache, - buf, - nbyte, - AtomicLoad(&file->offset)); - } - - if ((n_err.err != NULL) && strcmp(n_err.err, "io.EOF")) { - FILE_ERROR(ERRNO, -3); - } - -file_error:; -#ifdef DEBUG - if (errno == ENOSPC) - logf("No space left on device\n"); -#endif - - size_t done = n_err.n; - if (done) - { - /* error or not, update the file offset and size if anything was - transferred */ - AtomicAdd(&file->offset, done); -#ifndef LOGF_ENABLE /* wipes out log before you can save it */ - /* DEBUGF("file offset: %lld\n", file->offset); */ -#endif - /* adjust file size to length written */ - if (write && AtomicLoad(&file->offset) > size) - *file->sizep = AtomicLoad(&file->offset); - - return done; - } - - return rc; -} - -/* initialize the base descriptor */ -static void filestr_base_init(struct filestr_base *stream) -{ - stream->cache = NULL; - stream->handle = 0; - stream->size = 0; - LightLock_Init(&stream->mtx); -} - int open_internal_inner2(Handle *handle, const char *path, u32 openFlags, u32 attributes) { int rc; @@ -305,7 +97,7 @@ file_error: return rc; } -static int open_internal_inner1(const char *path, int oflag) +int open_internal_inner1(const char *path, int oflag) { int rc; struct filestr_desc *file; @@ -391,9 +183,9 @@ static int open_internal_inner1(const char *path, int oflag) file->stream.flags = oflag; file->stream.size = size; file->sizep = &file->stream.size; - AtomicSwap(&file->offset, 0); + AtomicSet(&file->offset, 0); - /* we will use file path to implement ctru_fsamefile function */ + /* we will use file path to implement console_fsamefile function */ strcpy(file->stream.path, path); return fildes; @@ -413,27 +205,7 @@ file_error: return rc; } -static int open_internal_locked(const char *path, int oflag) -{ - file_internal_lock_WRITER(); - int rc = open_internal_inner1(path, oflag); - file_internal_unlock_WRITER(); - return rc; -} - -int ctru_open(const char *path, int oflag, ...) -{ - logf("open(path=\"%s\",oflag=%X)\n", path, (unsigned)oflag); - return open_internal_locked(path, oflag); -} - -int ctru_creat(const char *path, mode_t mode) -{ - logf("creat(path=\"%s\")\n", path); - return ctru_open(path, O_WRONLY|O_CREAT|O_TRUNC, mode); -} - -int ctru_close(int fildes) +int console_close(int fildes) { logf("close(fd=%d)\n", fildes); @@ -466,7 +238,7 @@ file_error: } /* truncate a file to a specified length */ -int ctru_ftruncate(int fildes, off_t length) +int console_ftruncate(int fildes, off_t length) { logf("ftruncate(fd=%d,len=%ld)\n", fildes, (long)length); @@ -502,7 +274,7 @@ file_error: } /* synchronize changes to a file */ -int ctru_fsync(int fildes) +int console_fsync(int fildes) { logf("fsync(fd=%d)\n", fildes); @@ -530,79 +302,8 @@ file_error: return rc; } -/* move the read/write file offset */ -off_t ctru_lseek(int fildes, off_t offset, int whence) -{ -#ifndef LOGF_ENABLE /* wipes out log before you can save it */ - /* DEBUGF("lseek(fd=%d,ofs=%ld,wh=%d)\n", fildes, (long)offset, whence); */ -#endif - struct filestr_desc * const file = GET_FILESTR(READER, fildes); - if (!file) - FILE_ERROR_RETURN(ERRNO, -1); - - off_t rc = lseek_internal(file, offset, whence); - if (rc < 0) - FILE_ERROR(ERRNO, rc * 10 - 2); - -file_error: - RELEASE_FILESTR(READER, file); - return rc; -} - -/* read from a file */ -ssize_t ctru_read(int fildes, void *buf, size_t nbyte) -{ - struct filestr_desc * const file = GET_FILESTR(READER, fildes); - if (!file) - FILE_ERROR_RETURN(ERRNO, -1); - - ssize_t rc; - - if (file->stream.flags & O_WRONLY) - { - logf("read(fd=%d,buf=%p,nb=%lu) - " - "descriptor is write-only mode\n", - fildes, buf, (unsigned long)nbyte); - FILE_ERROR(EBADF, -2); - } - - rc = readwrite(file, buf, nbyte, false); - if (rc < 0) - FILE_ERROR(ERRNO, rc * 10 - 3); - -file_error: - RELEASE_FILESTR(READER, file); - return rc; -} - -/* write on a file */ -ssize_t ctru_write(int fildes, const void *buf, size_t nbyte) -{ - struct filestr_desc * const file = GET_FILESTR(READER, fildes); - if (!file) - FILE_ERROR_RETURN(ERRNO, -1); - - ssize_t rc; - - if (file->stream.flags & O_RDONLY) - { - logf("write(fd=%d,buf=%p,nb=%lu) - " - "descriptor is read-only mode\n", - fildes, buf, (unsigned long)nbyte); - FILE_ERROR(EBADF, -2); - } - - rc = readwrite(file, (void *)buf, nbyte, true); - if (rc < 0) - FILE_ERROR(ERRNO, rc * 10 - 3); - -file_error: - RELEASE_FILESTR(READER, file); - return rc; -} - /* remove a file */ -int ctru_remove(const char *path) +int console_remove(const char *path) { logf("remove(path=\"%s\")\n", path); @@ -621,7 +322,7 @@ file_error: } /* rename a file */ -int ctru_rename(const char *old, const char *new) +int console_rename(const char *old, const char *new) { /* note: move by rename does not work in devkitARM toolchain */ logf("rename(old=\"%s\",new=\"%s\")\n", old, new); @@ -698,7 +399,7 @@ file_error: /** Extensions **/ /* todo: utime does not work in devkitARM toolchain */ -int ctru_modtime(const char *path, time_t modtime) +int console_modtime(const char *path, time_t modtime) { struct utimbuf times = { @@ -709,66 +410,8 @@ int ctru_modtime(const char *path, time_t modtime) return utime(path, ×); } -/* get the binary size of a file (in bytes) */ -off_t ctru_filesize(int fildes) -{ - struct filestr_desc * const file = GET_FILESTR(READER, fildes); - if (!file) - FILE_ERROR_RETURN(ERRNO, -1); - - off_t rc; - file_size_t size = *file->sizep; - - if (size > FILE_SIZE_MAX) - FILE_ERROR(EOVERFLOW, -2); - - rc = (off_t)size; -file_error: - RELEASE_FILESTR(READER, file); - return rc; -} - -/* test if two file descriptors refer to the same file */ -int ctru_fsamefile(int fildes1, int fildes2) -{ - struct filestr_desc * const file1 = GET_FILESTR(WRITER, fildes1); - if (!file1) - FILE_ERROR_RETURN(ERRNO, -1); - - int rc = -2; - - struct filestr_desc * const file2 = get_filestr(fildes2); - if (file2) - rc = strcmp(file1->stream.path, file2->stream.path) == 0 ? 1 : 0; - - RELEASE_FILESTR(WRITER, file1); - return rc; -} - -/* tell the relationship of path1 to path2 */ -int ctru_relate(const char *path1, const char *path2) -{ - /* FAT32 file system does not support symbolic links, - therefore, comparing the two full paths should be enough - to tell relationship */ - logf("relate(path1=\"%s\",path2=\"%s\")\n", path1, path2); - int rc = RELATE_DIFFERENT; - if (strcmp(path1, path2) == 0) - rc = RELATE_SAME; - return rc; -} - -/* test file or directory existence */ -bool ctru_file_exists(const char *path) -{ - file_internal_lock_WRITER(); - bool rc = test_stream_exists_internal(path) > 0; - file_internal_unlock_WRITER(); - return rc; -} - /* note: no symbolic links support in devkitARM */ -ssize_t ctru_readlink(const char *path, char *buf, size_t bufsiz) +ssize_t console_readlink(const char *path, char *buf, size_t bufsiz) { return readlink(path, buf, bufsiz); } diff --git a/firmware/target/hosted/game_console/lib/sys_file_common.c b/firmware/target/hosted/game_console/lib/sys_file_common.c new file mode 100644 index 0000000000..2eab3dd4a3 --- /dev/null +++ b/firmware/target/hosted/game_console/lib/sys_file_common.c @@ -0,0 +1,384 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025 Mauricio G. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include +#include +#include +#include +#include +#include "fs_defines.h" + +/* this includes a couple of 3ds headers */ +#include "sys_file.h" +#include "sys_file_internal.h" + +#define RB_FILESYSTEM_OS +#include "config.h" +#include "system.h" +#include "file.h" +#include "debug.h" +#include "string-extra.h" + +/* This file is based on firmware/common/file.c */ + +/* Define LOGF_ENABLE to enable logf output in this file */ +// #define LOGF_ENABLE +#include "logf.h" + +/** + * These functions provide a roughly POSIX-compatible file I/O API. + * Important: the bufferio I/O library (bfile) used in the 3ds does not work + * with write-only files due to an internal limitation. + * So all files will be opened with the read flag by default. + */ + +struct filestr_desc open_streams[MAX_OPEN_FILES] = +{ + [0 ... MAX_OPEN_FILES-1] = { .stream = { .cache = NULL, .flags = 0 } } +}; + +/* check and return a struct filestr_desc* from a file descriptor number */ +struct filestr_desc * get_filestr(int fildes) +{ + struct filestr_desc *file = &open_streams[fildes]; + + if ((unsigned int)fildes >= MAX_OPEN_FILES) + file = NULL; + else if (file->stream.cache != NULL) + return file; + + logf("fildes %d: bad file number\n", fildes); + errno = (file && (file->stream.cache == NULL)) ? ENXIO : EBADF; + return NULL; +} + +/* find a free file descriptor */ +int alloc_filestr(struct filestr_desc **filep) +{ + for (int fildes = 0; fildes < MAX_OPEN_FILES; fildes++) + { + struct filestr_desc *file = &open_streams[fildes]; + if (file->stream.cache == NULL) + { + *filep = file; + return fildes; + } + } + + logf("Too many files open\n"); + return -1; +} + +/* set the file pointer */ +off_t lseek_internal(struct filestr_desc *file, off_t offset, + int whence) +{ + off_t rc; + off_t pos; + + off_t size = MIN(*file->sizep, FILE_SIZE_MAX); + off_t file_offset = AtomicGet(&file->offset); + + switch (whence) + { + case SEEK_SET: + if (offset < 0 || (off_t)offset > size) + FILE_ERROR(EINVAL, -1); + + pos = offset; + break; + + case SEEK_CUR: + if ((offset < 0 && (off_t)-offset > file_offset) || + (offset > 0 && (off_t)offset > size - file_offset)) + FILE_ERROR(EINVAL, -1); + + pos = file_offset + offset; + break; + + case SEEK_END: + if (offset > 0 || (off_t)-offset > size) + FILE_ERROR(EINVAL, -1); + + pos = size + offset; + break; + + default: + FILE_ERROR(EINVAL, -1); + } + + AtomicSet(&file->offset, pos); + + return pos; +file_error: + return rc; +} + +/* read from or write to the file; back end to read() and write() */ +ssize_t readwrite(struct filestr_desc *file, void *buf, size_t nbyte, + bool write) +{ +#ifndef LOGF_ENABLE /* wipes out log before you can save it */ + /* DEBUGF("readwrite(%p,%lx,%lu,%s)\n", + file, (long)buf, (unsigned long)nbyte, write ? "write" : "read"); */ +#endif + + const file_size_t size = *file->sizep; + size_t filerem; + + if (write) + { + /* if opened in append mode, move pointer to end */ + if (file->stream.flags & O_APPEND) + AtomicSet(&file->offset, MIN(size, FILE_SIZE_MAX)); + + filerem = FILE_SIZE_MAX - AtomicGet(&file->offset); + } + else + { + /* limit to maximum possible offset (EOF or FILE_SIZE_MAX) */ + filerem = MIN(size, FILE_SIZE_MAX) - AtomicGet(&file->offset); + } + + if (nbyte > filerem) + { + nbyte = filerem; + if (nbyte > 0) + {} + else if (write) + FILE_ERROR_RETURN(EFBIG, -1); /* would get too large */ + else if (AtomicGet(&file->offset) >= FILE_SIZE_MAX) + FILE_ERROR_RETURN(EOVERFLOW, -2); /* can't read here */ + } + + if (nbyte == 0) + return 0; + + int rc = 0; + int_error_t n_err; + + if (write) { + n_err = PageReader_WriteAt(file->stream.cache, + buf, + nbyte, + AtomicGet(&file->offset), + file->stream.flags & O_RDWR ? true : false); + } + else { + n_err = PageReader_ReadAt(file->stream.cache, + buf, + nbyte, + AtomicGet(&file->offset)); + } + + if ((n_err.err != NULL) && strcmp(n_err.err, "io.EOF")) { + FILE_ERROR(ERRNO, -3); + } + +file_error:; +#ifdef DEBUG + if (errno == ENOSPC) + logf("No space left on device\n"); +#endif + + size_t done = n_err.n; + if (done) + { + /* error or not, update the file offset and size if anything was + transferred */ + AtomicAdd(&file->offset, done); +#ifndef LOGF_ENABLE /* wipes out log before you can save it */ + /* DEBUGF("file offset: %lld\n", file->offset); */ +#endif + /* adjust file size to length written */ + if (write && AtomicGet(&file->offset) > size) + *file->sizep = AtomicGet(&file->offset); + + return done; + } + + return rc; +} + +/* initialize the base descriptor */ +void filestr_base_init(struct filestr_base *stream) +{ + stream->cache = NULL; + stream->handle = 0; + stream->size = 0; + sys_mutex_init(&stream->mtx); +} + +int open_internal_inner1(const char *path, int oflag); +int open_internal_locked(const char *path, int oflag) +{ + file_internal_lock_WRITER(); + int rc = open_internal_inner1(path, oflag); + file_internal_unlock_WRITER(); + return rc; +} + +int console_open(const char *path, int oflag, ...) +{ + logf("open(path=\"%s\",oflag=%X)\n", path, (unsigned)oflag); + + /* we need to wait a little here when playing a folder with many small files + to prevent other threads starvation */ + sleep(10); + + return open_internal_locked(path, oflag); +} + +int console_creat(const char *path, mode_t mode) +{ + logf("creat(path=\"%s\")\n", path); + return console_open(path, O_WRONLY|O_CREAT|O_TRUNC, mode); +} + +/* move the read/write file offset */ +off_t console_lseek(int fildes, off_t offset, int whence) +{ +#ifndef LOGF_ENABLE /* wipes out log before you can save it */ + /* DEBUGF("lseek(fd=%d,ofs=%ld,wh=%d)\n", fildes, (long)offset, whence); */ +#endif + struct filestr_desc * const file = GET_FILESTR(READER, fildes); + if (!file) + FILE_ERROR_RETURN(ERRNO, -1); + + off_t rc = lseek_internal(file, offset, whence); + if (rc < 0) + FILE_ERROR(ERRNO, rc * 10 - 2); + +file_error: + RELEASE_FILESTR(READER, file); + return rc; +} + +/* read from a file */ +ssize_t console_read(int fildes, void *buf, size_t nbyte) +{ + struct filestr_desc * const file = GET_FILESTR(READER, fildes); + if (!file) + FILE_ERROR_RETURN(ERRNO, -1); + + ssize_t rc; + + if (file->stream.flags & O_WRONLY) + { + logf("read(fd=%d,buf=%p,nb=%lu) - " + "descriptor is write-only mode\n", + fildes, buf, (unsigned long)nbyte); + FILE_ERROR(EBADF, -2); + } + + rc = readwrite(file, buf, nbyte, false); + if (rc < 0) + FILE_ERROR(ERRNO, rc * 10 - 3); + +file_error: + RELEASE_FILESTR(READER, file); + return rc; +} + +/* write on a file */ +ssize_t console_write(int fildes, const void *buf, size_t nbyte) +{ + struct filestr_desc * const file = GET_FILESTR(READER, fildes); + if (!file) + FILE_ERROR_RETURN(ERRNO, -1); + + ssize_t rc; + + if (file->stream.flags & O_RDONLY) + { + logf("write(fd=%d,buf=%p,nb=%lu) - " + "descriptor is read-only mode\n", + fildes, buf, (unsigned long)nbyte); + FILE_ERROR(EBADF, -2); + } + + rc = readwrite(file, (void *)buf, nbyte, true); + if (rc < 0) + FILE_ERROR(ERRNO, rc * 10 - 3); + +file_error: + RELEASE_FILESTR(READER, file); + return rc; +} + +/** Extensions **/ + +/* get the binary size of a file (in bytes) */ +off_t console_filesize(int fildes) +{ + struct filestr_desc * const file = GET_FILESTR(READER, fildes); + if (!file) + FILE_ERROR_RETURN(ERRNO, -1); + + off_t rc; + file_size_t size = *file->sizep; + + if (size > FILE_SIZE_MAX) + FILE_ERROR(EOVERFLOW, -2); + + rc = (off_t)size; +file_error: + RELEASE_FILESTR(READER, file); + return rc; +} + +/* test if two file descriptors refer to the same file */ +int console_fsamefile(int fildes1, int fildes2) +{ + struct filestr_desc * const file1 = GET_FILESTR(WRITER, fildes1); + if (!file1) + FILE_ERROR_RETURN(ERRNO, -1); + + int rc = -2; + + struct filestr_desc * const file2 = get_filestr(fildes2); + if (file2) + rc = strcmp(file1->stream.path, file2->stream.path) == 0 ? 1 : 0; + + RELEASE_FILESTR(WRITER, file1); + return rc; +} + +/* tell the relationship of path1 to path2 */ +int console_relate(const char *path1, const char *path2) +{ + /* FAT32 file system does not support symbolic links, + therefore, comparing the two full paths should be enough + to tell relationship */ + logf("relate(path1=\"%s\",path2=\"%s\")\n", path1, path2); + int rc = RELATE_DIFFERENT; + if (strcmp(path1, path2) == 0) + rc = RELATE_SAME; + return rc; +} + +/* test file or directory existence */ +int test_stream_exists_internal(const char *path); +bool console_file_exists(const char *path) +{ + file_internal_lock_WRITER(); + bool rc = test_stream_exists_internal(path) > 0; + file_internal_unlock_WRITER(); + return rc; +} \ No newline at end of file diff --git a/firmware/target/hosted/game_console/lib/sys_file_internal.h b/firmware/target/hosted/game_console/lib/sys_file_internal.h new file mode 100644 index 0000000000..cb800c7dd9 --- /dev/null +++ b/firmware/target/hosted/game_console/lib/sys_file_internal.h @@ -0,0 +1,63 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025 by Mauricio G. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef _SYS_FILE_INTERNAL_H +#define _SYS_FILE_INTERNAL_H + +/* structure used for open file descriptors */ +struct filestr_base; +struct filestr_desc +{ + struct filestr_base stream; /* basic stream info (first!) */ + file_size_t offset; /* current offset for stream */ + u64 *sizep; /* shortcut to file size in fileobj */ +}; + +extern struct filestr_desc open_streams[MAX_OPEN_FILES]; + +int test_stream_exists_internal(const char *path); +int alloc_filestr(struct filestr_desc **filep); +struct filestr_desc * get_filestr(int fildes); +off_t lseek_internal(struct filestr_desc *file, off_t offset, + int whence); +ssize_t readwrite(struct filestr_desc *file, void *buf, size_t nbyte, + bool write); +void filestr_base_init(struct filestr_base *stream); +int open_internal_locked(const char *path, int oflag); + +#define GET_FILESTR(type, fildes) \ + ({ \ + file_internal_lock_##type(); \ + struct filestr_desc * _file = get_filestr(fildes); \ + if (_file) \ + FILESTR_LOCK(type, &_file->stream); \ + else { \ + file_internal_unlock_##type(); \ + }\ + _file; \ + }) + +/* release the lock on the filestr_desc* */ +#define RELEASE_FILESTR(type, file) \ + ({ \ + FILESTR_UNLOCK(type, &(file)->stream); \ + file_internal_unlock_##type(); \ + }) +#endif /* _SYS_FILE_INTERNAL_H */ + diff --git a/firmware/target/hosted/ctru/lib/sys_thread.h b/firmware/target/hosted/game_console/lib/sys_thread.h similarity index 68% rename from firmware/target/hosted/ctru/lib/sys_thread.h rename to firmware/target/hosted/game_console/lib/sys_thread.h index d6dea134c5..cbdc294720 100644 --- a/firmware/target/hosted/ctru/lib/sys_thread.h +++ b/firmware/target/hosted/game_console/lib/sys_thread.h @@ -21,20 +21,11 @@ #ifndef __SYSTHREAD_H__ #define __SYSTHREAD_H__ -#include <3ds/synchronization.h> -#include <3ds/thread.h> -#include <3ds/services/apt.h> +#include "sys_types.h" +#include "sys_atomic.h" #include "thread.h" -/* Complementary atomic operations */ -bool _AtomicCAS(u32 *ptr, int oldval, int newval); -#define AtomicGet(ptr) __atomic_load_n((u32*)(ptr), __ATOMIC_SEQ_CST) -#define AtomicSet(ptr, value) AtomicSwap(ptr, value) -#define AtomicCAS(ptr, oldvalue, newvalue) _AtomicCAS((u32 *)(ptr), oldvalue, newvalue) -void AtomicLock(int *lock); -void AtomicUnlock(int *lock); - /* This code was taken from SDL2 thread implementation */ enum thread_state_t @@ -48,7 +39,7 @@ enum thread_state_t typedef struct _thread { int threadid; - Thread handle; + sys_thread_type handle; int status; int state; size_t stacksize; @@ -57,31 +48,52 @@ typedef struct _thread void *data; } sysThread; +typedef struct _mutex +{ + sys_mutex_type lock; +} sysMutex; + +typedef struct _sem +{ + sys_sem_type semaphore; +} sysSem; + typedef struct _cond { - RecursiveLock lock; + sysMutex lock; int waiting; int signals; - LightSemaphore wait_sem; - LightSemaphore wait_done; + sysSem wait_sem; + sysSem wait_done; } sysCond; -int sys_sem_wait(LightSemaphore *sem); -int sys_sem_wait_timeout(LightSemaphore *sem, u32 timeout); -int sys_sem_try_wait(LightSemaphore *sem); -u32 sys_sem_value(LightSemaphore *sem); +sysMutex *sys_mutex_create(void); +void sys_mutex_init(sysMutex *mutex); +int sys_mutex_lock(sysMutex *mutex); +int sys_mutex_try_lock(sysMutex *mutex); +int sys_mutex_unlock(sysMutex *mutex); -sysThread *sys_create_thread(int(*fn)(void *), const char *name, const size_t stacksize, +sysSem *sys_sem_create(u32 initial_value); +void sys_sem_init(sysSem *sem, u32 initial_value); +int sys_sem_wait(sysSem *sem); +int sys_sem_wait_timeout(sysSem *sem, u32 timeout); +int sys_sem_try_wait(sysSem *sem); +u32 sys_sem_value(sysSem *sem); +int sys_sem_post(sysSem *sem); + +sysCond *sys_cond_create(void); +void sys_cond_init(sysCond *cond); +void sys_cond_destroy(sysCond *cond); +int sys_cond_signal(sysCond *cond); +int sys_cond_broadcast(sysCond *cond); +int sys_cond_wait(sysCond *cond, sysMutex *mutex); + +sysThread *sys_thread_create(int(*fn)(void *), const char *name, const size_t stacksize, void *data IF_PRIO(, int priority) IF_COP(, unsigned int core)); void sys_run_thread(sysThread *thread); void sys_wait_thread(sysThread *thread, int *status); int sys_thread_id(void); int sys_set_thread_priority(sysThread *thread, int priority); -sysCond *sys_cond_create(void); -void sys_cond_destroy(sysCond *cond); -int sys_cond_signal(sysCond *cond); -int sys_cond_broadcast(sysCond *cond); -int sys_cond_wait(sysCond *cond, RecursiveLock *mutex); #endif /* #ifndef __SYSTHREAD_H__ */ diff --git a/firmware/target/hosted/ctru/lib/sys_thread.c b/firmware/target/hosted/game_console/lib/sys_thread_3ds.c similarity index 58% rename from firmware/target/hosted/ctru/lib/sys_thread.c rename to firmware/target/hosted/game_console/lib/sys_thread_3ds.c index 1634fb3d7f..d280aa39f2 100644 --- a/firmware/target/hosted/ctru/lib/sys_thread.c +++ b/firmware/target/hosted/game_console/lib/sys_thread_3ds.c @@ -28,49 +28,129 @@ #include "debug.h" #include "logf.h" -bool _AtomicCAS(u32 *ptr, int oldval, int newval) +/* sysMutex */ +sysMutex *sys_mutex_create(void) { - int expected = oldval; - int desired = newval; - return __atomic_compare_exchange(ptr, &expected, &desired, false, - __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); -} - -bool _AtomicTryLock(int *lock) -{ - int result; - asm volatile( - "ldrex %0, [%2] \n" - "teq %0, #0 \n" - "strexeq %0, %1, [%2] \n" - : "=&r"(result) - : "r"(1), "r"(lock) - : "cc", "memory" - ); - return result == 0; -} - -#define CPUPauseInstruction() asm volatile("yield" ::: "memory") -void AtomicLock(int *lock) -{ - int iterations = 0; - while (!_AtomicTryLock(lock)) { - if (iterations < 32) { - iterations++; - CPUPauseInstruction(); - } else { - sys_delay(0); - } + sysMutex *m = (sysMutex *) malloc(sizeof(sysMutex)); + if (m == NULL) { + DEBUGF("sys_create_mutex: no memory\n"); + return NULL; } + + sys_mutex_init(m); + return m; } -void AtomicUnlock(int *lock) +void sys_mutex_init(sysMutex *mutex) { - *lock = 0; + memset(mutex, 0, sizeof(sysMutex)); + RecursiveLock_Init(&mutex->lock); } +int sys_mutex_lock(sysMutex *mutex) +{ + RecursiveLock_Lock(&mutex->lock); + return 0; +} + +int sys_mutex_try_lock(sysMutex *mutex) +{ + int res = RecursiveLock_TryLock(&mutex->lock); + if (res < 0) { + logf("RecursiveLock_TryLock: %d", res); + return -1; + } + + return 0; +} + +int sys_mutex_unlock(sysMutex *mutex) +{ + RecursiveLock_Unlock(&mutex->lock); + return 0; +} + +/* sysSem */ +void sys_sem_init(sysSem *sem, u32 initial_value) +{ + LightSemaphore_Init(&sem->semaphore, initial_value, ((s16)0x7FFF)); +} + +sysSem *sys_sem_create(u32 initial_value) +{ + sysSem *s = (sysSem *) malloc(sizeof(sysSem)); + if (s == NULL) + { + DEBUGF("sys_create_sem: no memory\n"); + return NULL; + } + + sys_sem_init(s, initial_value); + return s; +} + +int wait_on_semaphore_for(sysSem *sem, u32 timeout) +{ + u64 stop_time = sys_get_ticks64() + timeout; + u64 current_time = sys_get_ticks64(); + while (current_time < stop_time) { + if (LightSemaphore_TryAcquire(&sem->semaphore, 1) == 0) { + return 0; + } + /* 100 microseconds seems to be the sweet spot */ + svcSleepThread(100000LL); + current_time = sys_get_ticks64(); + } + + /* If we failed, yield to avoid starvation on busy waits */ + svcSleepThread(1); + return 1; +} + +int sys_sem_try_wait(sysSem *sem) +{ + if (LightSemaphore_TryAcquire(&sem->semaphore, 1) != 0) { + /* If we failed, yield to avoid starvation on busy waits */ + svcSleepThread(1); + return 1; + } + + return 0; +} + +int sys_sem_wait_timeout(sysSem *sem, u32 timeout) +{ + if (timeout == (~(u32)0)) { + LightSemaphore_Acquire(&sem->semaphore, 1); + return 0; + } + + if (LightSemaphore_TryAcquire(&sem->semaphore, 1) != 0) { + return wait_on_semaphore_for(sem, timeout); + } + + return 0; +} + +int sys_sem_wait(sysSem *sem) +{ + return sys_sem_wait_timeout(sem, (~(u32)0)); +} + +u32 sys_sem_value(sysSem *sem) +{ + return sem->semaphore.current_count; +} + +int sys_sem_post(sysSem *sem) +{ + LightSemaphore_Release(&sem->semaphore, 1); + return 0; +} + +/* sysThread */ /* Convert rockbox priority value to libctru value */ -int get_ctru_thread_priority(int priority) +int get_3ds_thread_priority(int priority) { if ((priority == PRIORITY_REALTIME_1) || (priority == PRIORITY_REALTIME_2) || (priority == PRIORITY_REALTIME_3) || (priority == PRIORITY_REALTIME_4) || @@ -106,59 +186,6 @@ static void thread_entry(void *arg) threadExit(0); } -int wait_on_semaphore_for(LightSemaphore *sem, u32 timeout) -{ - u64 stop_time = sys_get_ticks64() + timeout; - u64 current_time = sys_get_ticks64(); - while (current_time < stop_time) { - if (LightSemaphore_TryAcquire(sem, 1) == 0) { - return 0; - } - /* 100 microseconds seems to be the sweet spot */ - svcSleepThread(100000LL); - current_time = sys_get_ticks64(); - } - - /* If we failed, yield to avoid starvation on busy waits */ - svcSleepThread(1); - return 1; -} - -int sys_sem_try_wait(LightSemaphore *sem) -{ - if (LightSemaphore_TryAcquire(sem, 1) != 0) { - /* If we failed, yield to avoid starvation on busy waits */ - svcSleepThread(1); - return 1; - } - - return 0; -} - -int sys_sem_wait_timeout(LightSemaphore *sem, u32 timeout) -{ - if (timeout == (~(u32)0)) { - LightSemaphore_Acquire(sem, 1); - return 0; - } - - if (LightSemaphore_TryAcquire(sem, 1) != 0) { - return wait_on_semaphore_for(sem, timeout); - } - - return 0; -} - -int sys_sem_wait(LightSemaphore *sem) -{ - return sys_sem_wait_timeout(sem, (~(u32)0)); -} - -u32 sys_sem_value(LightSemaphore *sem) -{ - return sem->current_count; -} - int sys_thread_id(void) { u32 thread_ID = 0; @@ -188,7 +215,7 @@ void sys_run_thread(sysThread *thread) } } -sysThread *sys_create_thread(int(*fn)(void *), const char *name, const size_t stacksize, +sysThread *sys_thread_create(int(*fn)(void *), const char *name, const size_t stacksize, void *data IF_PRIO(, int priority) IF_COP(, unsigned int core)) { sys_ticks_init(); @@ -216,7 +243,7 @@ sysThread *sys_create_thread(int(*fn)(void *), const char *name, const size_t st thread->handle = threadCreate(thread_entry, thread, get_thread_stack_size(stacksize), - get_ctru_thread_priority(priority), + get_3ds_thread_priority(priority), cpu, false); @@ -275,127 +302,10 @@ int sys_set_thread_priority(sysThread *thread, int priority) { Handle h = threadGetHandle(thread->handle); int old_priority = priority; - Result res = svcSetThreadPriority(h, get_ctru_thread_priority(priority)); + Result res = svcSetThreadPriority(h, get_3ds_thread_priority(priority)); if (R_SUCCEEDED(res)) { return priority; } return old_priority; -} - -/* sysCond */ -sysCond *sys_cond_create(void) -{ - sysCond *cond; - - cond = (sysCond *)malloc(sizeof(sysCond)); - if (cond) { - RecursiveLock_Init(&cond->lock); - LightSemaphore_Init(&cond->wait_sem, 0, ((s16)0x7FFF)); - LightSemaphore_Init(&cond->wait_done, 0, ((s16)0x7FFF)); - cond->waiting = cond->signals = 0; - } else { - DEBUGF("sys_cond_create: out of memory.\n");; - } - return cond; -} - -/* Destroy a condition variable */ -void sys_cond_destroy(sysCond *cond) -{ - if (cond) { - free(cond); - } -} - -/* Restart one of the threads that are waiting on the condition variable */ -int sys_cond_signal(sysCond *cond) -{ - if (!cond) { - DEBUGF("sys_cond_signal: Invalid param 'cond'\n"); - return -1; - } - - /* If there are waiting threads not already signalled, then - signal the condition and wait for the thread to respond. - */ - RecursiveLock_Lock(&cond->lock); - if (cond->waiting > cond->signals) { - ++cond->signals; - LightSemaphore_Release(&cond->wait_sem, 1); - RecursiveLock_Unlock(&cond->lock); - LightSemaphore_Acquire(&cond->wait_done, 1); - } else { - RecursiveLock_Unlock(&cond->lock); - } - - return 0; -} - -/* Restart all threads that are waiting on the condition variable */ -int sys_cond_broadcast(sysCond *cond) -{ - if (!cond) { - DEBUGF("sys_cond_signal: Invalid param 'cond'\n"); - return -1; - } - - /* If there are waiting threads not already signalled, then - signal the condition and wait for the thread to respond. - */ - RecursiveLock_Lock(&cond->lock); - if (cond->waiting > cond->signals) { - int i, num_waiting; - - num_waiting = (cond->waiting - cond->signals); - cond->signals = cond->waiting; - for (i = 0; i < num_waiting; ++i) { - LightSemaphore_Release(&cond->wait_sem, 1); - } - /* Now all released threads are blocked here, waiting for us. - Collect them all (and win fabulous prizes!) :-) - */ - RecursiveLock_Unlock(&cond->lock); - for (i = 0; i < num_waiting; ++i) { - LightSemaphore_Acquire(&cond->wait_done, 1); - } - } else { - RecursiveLock_Unlock(&cond->lock); - } - - return 0; -} - -int sys_cond_wait(sysCond *cond, RecursiveLock *mutex) -{ - if (!cond) { - DEBUGF("sys_cond_signal: Invalid param 'cond'\n"); - return -1; - } - - RecursiveLock_Lock(&cond->lock); - ++cond->waiting; - RecursiveLock_Unlock(&cond->lock); - - /* Unlock the mutex, as is required by condition variable semantics */ - RecursiveLock_Unlock(mutex); - - /* Wait for a signal */ - LightSemaphore_Acquire(&cond->wait_sem, 1); - - RecursiveLock_Lock(&cond->lock); - if (cond->signals > 0) { - /* We always notify the signal thread that we are done */ - LightSemaphore_Release(&cond->wait_done, 1); - - /* Signal handshake complete */ - --cond->signals; - } - --cond->waiting; - RecursiveLock_Unlock(&cond->lock); - - /* Lock the mutex, as is required by condition variable semantics */ - RecursiveLock_Lock(mutex); - - return 0; -} +} \ No newline at end of file diff --git a/firmware/target/hosted/ctru/lib/sys_timer.h b/firmware/target/hosted/game_console/lib/sys_timer.h similarity index 100% rename from firmware/target/hosted/ctru/lib/sys_timer.h rename to firmware/target/hosted/game_console/lib/sys_timer.h diff --git a/firmware/target/hosted/game_console/lib/sys_timer_3ds.c b/firmware/target/hosted/game_console/lib/sys_timer_3ds.c new file mode 100644 index 0000000000..8500567688 --- /dev/null +++ b/firmware/target/hosted/game_console/lib/sys_timer_3ds.c @@ -0,0 +1,71 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2006 Dan Everton + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include +#include +#include +#include + +#include <3ds/os.h> + +#include "debug.h" +#include "logf.h" + +static bool ticks_started = false; +static u64 start_tick; + +#define NSEC_PER_MSEC 1000000ULL + +void sys_ticks_init(void) +{ + if (ticks_started) { + return; + } + ticks_started = true; + + start_tick = svcGetSystemTick(); +} + +void sys_ticks_quit(void) +{ + ticks_started = false; +} + +u64 sys_get_ticks64(void) +{ + u64 elapsed; + if (!ticks_started) { + sys_ticks_init(); + } + + elapsed = svcGetSystemTick() - start_tick; + return elapsed / CPU_TICKS_PER_MSEC; +} + +u32 sys_get_ticks(void) +{ + return (u32)(sys_get_ticks64() & 0xFFFFFFFF); +} + +void sys_delay(u32 ms) +{ + svcSleepThread(ms * NSEC_PER_MSEC); +} \ No newline at end of file diff --git a/firmware/target/hosted/ctru/lib/sys_timer.c b/firmware/target/hosted/game_console/lib/sys_timer_common.c similarity index 84% rename from firmware/target/hosted/ctru/lib/sys_timer.c rename to firmware/target/hosted/game_console/lib/sys_timer_common.c index 8cc5fa334a..8fea61df21 100644 --- a/firmware/target/hosted/ctru/lib/sys_timer.c +++ b/firmware/target/hosted/game_console/lib/sys_timer_common.c @@ -24,8 +24,6 @@ #include #include -#include <3ds/os.h> -/* these include a couple of 3ds headers */ #include "sys_thread.h" #include "sys_timer.h" @@ -34,47 +32,7 @@ #define CACHELINE_SIZE 128 -static bool ticks_started = false; -static u64 start_tick; - -#define NSEC_PER_MSEC 1000000ULL - -void sys_ticks_init(void) -{ - if (ticks_started) { - return; - } - ticks_started = true; - - start_tick = svcGetSystemTick(); -} - -void sys_ticks_quit(void) -{ - ticks_started = false; -} - -u64 sys_get_ticks64(void) -{ - u64 elapsed; - if (!ticks_started) { - sys_ticks_init(); - } - - elapsed = svcGetSystemTick() - start_tick; - return elapsed / CPU_TICKS_PER_MSEC; -} - -u32 sys_get_ticks(void) -{ - return (u32)(sys_get_ticks64() & 0xFFFFFFFF); -} - -void sys_delay(u32 ms) -{ - svcSleepThread(ms * NSEC_PER_MSEC); -} - +/* sysTimer */ typedef struct _timer { int timerID; @@ -97,17 +55,17 @@ typedef struct _timer_map typedef struct { /* Data used by the main thread */ - Thread thread; + sysThread *thread; int nextID; timerMap *timermap; - RecursiveLock timermap_lock; + sysMutex *timermap_lock; /* Padding to separate cache lines between threads */ char cache_pad[CACHELINE_SIZE]; /* Data used to communicate with the timer thread */ int lock; - LightSemaphore sem; + sysSem sem; sysTimer *pending; sysTimer *freelist; int active; @@ -144,9 +102,9 @@ static void add_timer_interval(timerData *data, sysTimer *timer) timer->next = curr; } -static void timer_thread(void *_data) +static int timer_thread(void *_data) { - timerData *data = (timerData *)_data; + timerData *data = (timerData *) _data; sysTimer *pending; sysTimer *current; sysTimer *freelist_head = NULL; @@ -246,6 +204,8 @@ static void timer_thread(void *_data) */ sys_sem_wait_timeout(&data->sem, delay); } + + return 0; } int sys_timer_init(void) @@ -253,17 +213,22 @@ int sys_timer_init(void) timerData *data = &timer_data; if (!AtomicGet(&data->active)) { - RecursiveLock_Init(&data->timermap_lock); - LightSemaphore_Init(&data->sem, 0, ((s16)0x7FFF)); + data->timermap_lock = (sysMutex *) malloc(sizeof(sysMutex)); + if (!data->timermap_lock) { + return -1; + } + sys_mutex_init(data->timermap_lock); + sys_sem_init(&data->sem, 0); + AtomicSet(&data->active, 1); /* Timer threads use a callback into the app, so we can't set a limited stack size here. */ - data->thread = threadCreate(timer_thread, - data, - 32 * 1024, - 0x28, - -1, - false); + data->thread = sys_thread_create(timer_thread, + "timer", + 32 * 1024, /* 32 kB */ + data + IF_PRIO( , PRIORITY_REALTIME ) /* Realtime priority */ + IF_COP( , 0 )); if (!data->thread) { sys_timer_quit(); return -1; @@ -283,9 +248,8 @@ void sys_timer_quit(void) if (AtomicCAS(&data->active, 1, 0)) { /* active? Move to inactive. */ /* Shutdown the timer thread */ if (data->thread) { - LightSemaphore_Release(&data->sem, 1); - threadJoin(data->thread, U64_MAX); - threadFree(data->thread); + sys_sem_post(&data->sem); + sys_wait_thread(data->thread, NULL); data->thread = NULL; } @@ -305,6 +269,9 @@ void sys_timer_quit(void) data->timermap = entry->next; free(entry); } + + free(data->timermap_lock); + data->timermap_lock = NULL; } } @@ -353,10 +320,10 @@ int sys_add_timer(u32 interval, timer_callback_ptr callback, void *param) entry->timer = timer; entry->timerID = timer->timerID; - RecursiveLock_Lock(&data->timermap_lock); + sys_mutex_lock(data->timermap_lock); entry->next = data->timermap; data->timermap = entry; - RecursiveLock_Unlock(&data->timermap_lock); + sys_mutex_unlock(data->timermap_lock); /* Add the timer to the pending list for the timer thread */ AtomicLock(&data->lock); @@ -365,7 +332,7 @@ int sys_add_timer(u32 interval, timer_callback_ptr callback, void *param) AtomicUnlock(&data->lock); /* Wake up the timer thread if necessary */ - LightSemaphore_Release(&data->sem, 1); + sys_sem_post(&data->sem); return entry->timerID; } @@ -377,7 +344,7 @@ bool sys_remove_timer(int id) bool canceled = false; /* Find the timer */ - RecursiveLock_Lock(&data->timermap_lock); + sys_mutex_lock(data->timermap_lock); prev = NULL; for (entry = data->timermap; entry; prev = entry, entry = entry->next) { if (entry->timerID == id) { @@ -389,7 +356,7 @@ bool sys_remove_timer(int id) break; } } - RecursiveLock_Unlock(&data->timermap_lock); + sys_mutex_unlock(data->timermap_lock); if (entry) { if (!AtomicGet(&entry->timer->canceled)) { @@ -399,5 +366,4 @@ bool sys_remove_timer(int id) free(entry); } return canceled; -} - +} \ No newline at end of file diff --git a/firmware/target/hosted/game_console/lib/sys_types.h b/firmware/target/hosted/game_console/lib/sys_types.h new file mode 100644 index 0000000000..f62670bffe --- /dev/null +++ b/firmware/target/hosted/game_console/lib/sys_types.h @@ -0,0 +1,40 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025 Mauricio G. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef __SYSTYPES_H__ +#define __SYSTYPES_H__ + +#ifdef N3DS +#include <3ds/svc.h> +#include <3ds/types.h> +#include <3ds/thread.h> +#include <3ds/result.h> +#include <3ds/services/apt.h> +#include <3ds/services/fs.h> +#include <3ds/synchronization.h> + +typedef Thread sys_thread_type; +typedef RecursiveLock sys_mutex_type; +typedef LightSemaphore sys_sem_type; +typedef Handle sys_handle_type; +#endif + +#endif /* __SYSTYPES_H__ */ + diff --git a/firmware/target/hosted/ctru/system-ctru.h b/firmware/target/hosted/game_console/system-console.h similarity index 100% rename from firmware/target/hosted/ctru/system-ctru.h rename to firmware/target/hosted/game_console/system-console.h diff --git a/firmware/target/hosted/ctru/system-target.h b/firmware/target/hosted/game_console/system-target.h similarity index 54% rename from firmware/target/hosted/ctru/system-target.h rename to firmware/target/hosted/game_console/system-target.h index 79df842405..6ec0f3ae7f 100644 --- a/firmware/target/hosted/ctru/system-target.h +++ b/firmware/target/hosted/game_console/system-target.h @@ -1,4 +1,4 @@ -#include "system-ctru.h" +#include "system-console.h" #define NEED_GENERIC_BYTESWAPS diff --git a/firmware/target/hosted/ctru/thread-ctru.c b/firmware/target/hosted/game_console/thread-console.c similarity index 82% rename from firmware/target/hosted/ctru/thread-ctru.c rename to firmware/target/hosted/game_console/thread-console.c index 6dc057e333..3be2c59024 100644 --- a/firmware/target/hosted/ctru/thread-ctru.c +++ b/firmware/target/hosted/game_console/thread-console.c @@ -5,8 +5,9 @@ * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ + * $Id$ * - * Copyright (C) 2006 Dan Everton + * Copyright (C) 2026 Mauricio G. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -18,19 +19,19 @@ * ****************************************************************************/ -#include "autoconf.h" - #include #include #include #include /* memset() */ #include -#include "system-ctru.h" -#include "thread-ctru.h" -#include "sys_thread.h" +#include "system-console.h" +#include "thread-console.h" #include "../kernel-internal.h" #include "core_alloc.h" +#include "sys_thread.h" +#include "sys_timer.h" + /* Define this as 1 to show informational messages that are not errors. */ #define THREAD_DEBUGF_ENABLED 0 @@ -54,7 +55,7 @@ static jmp_buf thread_jmpbufs[MAXTHREADS]; /* this mutex locks out other Rockbox threads while one runs, * that enables us to simulate a cooperative environment even if * the host is preemptive */ -static RecursiveLock m; +static sysMutex *m; #define THREADS_RUN 0 #define THREADS_EXIT 1 #define THREADS_EXIT_COMMAND_DONE 2 @@ -78,7 +79,7 @@ void sim_thread_shutdown(void) threads_status = THREADS_EXIT; /* Take control */ - RecursiveLock_Lock(&m); + sys_mutex_lock(m); /* Signal all threads on delay or block */ for (i = 0; i < MAXTHREADS; i++) @@ -86,7 +87,7 @@ void sim_thread_shutdown(void) struct thread_entry *thread = __thread_slot_entry(i); if (thread->context.s == NULL) continue; - LightSemaphore_Release(thread->context.s, 1); + sys_sem_post(thread->context.s); } /* Wait for all threads to finish and cleanup old ones. */ @@ -97,11 +98,11 @@ void sim_thread_shutdown(void) if (t != NULL) { - RecursiveLock_Unlock(&m); + sys_mutex_unlock(m); /* Wait for it to finish */ sys_wait_thread(t, NULL); /* Relock for next thread signal */ - RecursiveLock_Lock(&m); + sys_mutex_lock(m); /* Already waited and exiting thread would have waited .told, * replacing it with t. */ thread->context.told = NULL; @@ -117,7 +118,7 @@ void sim_thread_shutdown(void) } } - RecursiveLock_Unlock(&m); + sys_mutex_unlock(m); /* Signal completion of operation */ threads_status = THREADS_EXIT_COMMAND_DONE; @@ -136,7 +137,7 @@ void sim_thread_exception_wait(void) /* A way to yield and leave the threading system for extended periods */ void sim_thread_lock(void *me) { - RecursiveLock_Lock(&m); + sys_mutex_lock(m); __running_self_entry() = (struct thread_entry *)me; if (threads_status != THREADS_RUN) @@ -146,7 +147,7 @@ void sim_thread_lock(void *me) void * sim_thread_unlock(void) { struct thread_entry *current = __running_self_entry(); - RecursiveLock_Unlock(&m); + sys_mutex_unlock(m); return current; } @@ -160,9 +161,9 @@ void switch_thread(void) { case STATE_RUNNING: { - RecursiveLock_Unlock(&m); + sys_mutex_unlock(m); /* Any other thread waiting already will get it first */ - RecursiveLock_Lock(&m); + sys_mutex_lock(m); break; } /* STATE_RUNNING: */ @@ -170,9 +171,9 @@ void switch_thread(void) { int oldlevel; - RecursiveLock_Unlock(&m); + sys_mutex_unlock(m); sys_sem_wait(current->context.s); - RecursiveLock_Lock(&m); + sys_mutex_lock(m); oldlevel = disable_irq_save(); current->state = STATE_RUNNING; @@ -184,9 +185,9 @@ void switch_thread(void) { int result, oldlevel; - RecursiveLock_Unlock(&m); + sys_mutex_unlock(m); result = sys_sem_wait_timeout(current->context.s, current->tmo_tick); - RecursiveLock_Lock(&m); + sys_mutex_lock(m); oldlevel = disable_irq_save(); @@ -207,9 +208,9 @@ void switch_thread(void) case STATE_SLEEPING: { - RecursiveLock_Unlock(&m); + sys_mutex_unlock(m); sys_sem_wait_timeout(current->context.s, current->tmo_tick); - RecursiveLock_Lock(&m); + sys_mutex_lock(m); current->state = STATE_RUNNING; break; } /* STATE_SLEEPING: */ @@ -252,7 +253,7 @@ void block_thread_(struct thread_entry *current, int ticks) } unsigned int wakeup_thread_(struct thread_entry *thread - IF_PRIO(, enum wakeup_thread_protocol proto)) + IF_PRIO(, enum wakeup_thread_protocol proto)) { switch (thread->state) { @@ -260,7 +261,7 @@ unsigned int wakeup_thread_(struct thread_entry *thread case STATE_BLOCKED_W_TMO: wait_queue_remove(thread); thread->state = STATE_RUNNING; - LightSemaphore_Release(thread->context.s, 1); + sys_sem_post(thread->context.s); return THREAD_OK; } @@ -275,15 +276,48 @@ void thread_thaw(unsigned int thread_id) if (thread->id == thread_id && thread->state == STATE_FROZEN) { thread->state = STATE_RUNNING; - LightSemaphore_Release(thread->context.s, 1); + sys_sem_post(thread->context.s); } } +#ifdef HAVE_SCHEDULER_BOOSTCTRL +/*--------------------------------------------------------------------------- + * Change the boost state of a thread boosting or unboosting the CPU + * as required. + *--------------------------------------------------------------------------- + */ +static inline void boost_thread(struct thread_entry *thread, bool boost) +{ + if ((thread->cpu_boost != 0) != boost) + { + thread->cpu_boost = boost; +#ifdef CPU_BOOST_LOGGING + const char fmt[] = __FILE__" thread[%s]"; + char pathbuf[sizeof(fmt) + 32]; /* thread name 32 */ + snprintf(pathbuf, sizeof(pathbuf), fmt, thread->name); + cpu_boost_(boost, pathbuf, __LINE__); +#else + cpu_boost(boost); +#endif + } +} + +void trigger_cpu_boost(void) +{ + boost_thread(__running_self_entry(), true); +} + +void cancel_cpu_boost(void) +{ + boost_thread(__running_self_entry(), false); +} +#endif + int runthread(void *data) { /* Cannot access thread variables before locking the mutex as the data structures may not be filled-in yet. */ - RecursiveLock_Lock(&m); + sys_mutex_lock(m); struct thread_entry *current = (struct thread_entry *)data; __running_self_entry() = current; @@ -296,18 +330,18 @@ int runthread(void *data) /* Run the thread routine */ if (current->state == STATE_FROZEN) { - RecursiveLock_Unlock(&m); + sys_mutex_unlock(m); sys_sem_wait(current->context.s); - RecursiveLock_Lock(&m); + sys_mutex_lock(m); __running_self_entry() = current; } if (threads_status == THREADS_RUN) { current->context.start(); - THREAD_DEBUGF("Thread Done: %d (%s)\n", - THREAD_ID_SLOT(current->id), - THREAD_GET_NAME(current)); + THREAD_DEBUGF("Thread Done: %ld (%s)\n", + THREAD_ID_SLOT(current->id), + THREAD_GET_NAME(current)); /* Thread routine returned - suicide */ } @@ -316,7 +350,7 @@ int runthread(void *data) else { /* Unlock and exit */ - RecursiveLock_Unlock(&m); + sys_mutex_unlock(m); } return 0; @@ -337,16 +371,14 @@ unsigned int create_thread(void (*function)(void), return 0; } - LightSemaphore *s = (LightSemaphore *) malloc(sizeof(LightSemaphore)); + sysSem *s = sys_sem_create(0); if (s == NULL) { DEBUGF("Failed to create semaphore\n"); return 0; } - LightSemaphore_Init(s, 0, 255); - - sysThread *t = sys_create_thread(runthread, + sysThread *t = sys_thread_create(runthread, name, stack_size, thread @@ -365,11 +397,10 @@ unsigned int create_thread(void (*function)(void), thread->context.start = function; thread->context.t = t; thread->context.s = s; - thread->priority = priority; THREAD_DEBUGF("New Thread: %lu (%s)\n", - (unsigned long)thread->id, - THREAD_GET_NAME(thread)); + (unsigned long)thread->id, + THREAD_GET_NAME(thread)); return thread->id; (void)stack; @@ -382,15 +413,14 @@ void thread_exit(void) int oldlevel = disable_irq_save(); sysThread *t = current->context.t; - LightSemaphore *s = current->context.s; + sysSem *s = current->context.s; - /* Wait the last thread here and keep this one or ctru will leak it since + /* Wait the last thread here and keep this one or the app will leak it since * it doesn't free its own library allocations unless a wait is performed. * Such behavior guards against the memory being invalid by the time * sys_wait_thread is reached and also against two different threads having * the same pointer. It also makes sys_wait_thread a non-concurrent function. * - * However, see more below about sys_kill_thread. */ sys_wait_thread(current->context.told, NULL); @@ -448,8 +478,18 @@ int thread_get_priority(unsigned int thread_id) /* Initialize threading */ void init_threads(void) { - RecursiveLock_Init(&m); - RecursiveLock_Lock(&m); + m = sys_mutex_create(); + if (m == NULL) + { + fprintf(stderr, "Failed to create mutex\n"); + return; + } + + if (sys_mutex_lock(m) == -1) + { + fprintf(stderr, "Couldn't lock mutex\n"); + return; + } thread_alloc_init(); @@ -461,12 +501,11 @@ void init_threads(void) } /* Slot 0 is reserved for the main thread - initialize it here and - then create the thread - it is possible to have a quick, early - shutdown try to access the structure. */ + then create the system thread - it is possible to have a quick, + early shutdown try to access the structure. */ thread->name = __main_thread_name; thread->state = STATE_RUNNING; - thread->context.s = (LightSemaphore *) malloc(sizeof(LightSemaphore)); - LightSemaphore_Init(thread->context.s, 0, 255); + thread->context.s = sys_sem_create(0); thread->context.t = NULL; /* NULL for the implicit main thread */ __running_self_entry() = thread; @@ -485,18 +524,19 @@ void init_threads(void) if (setjmp(thread_jmpbufs[THREAD_ID_SLOT(thread->id)]) == 0) { THREAD_DEBUGF("Main Thread: %lu (%s)\n", - (unsigned long)thread->id, - THREAD_GET_NAME(thread)); + (unsigned long)thread->id, + THREAD_GET_NAME(thread)); return; } - RecursiveLock_Unlock(&m); + sys_mutex_unlock(m); /* Set to 'COMMAND_DONE' when other rockbox threads have exited. */ while (threads_status < THREADS_EXIT_COMMAND_DONE) sys_delay(10); + free(m); + /* We're the main thead - perform exit - doesn't return. */ sim_do_exit(); } - diff --git a/firmware/target/hosted/ctru/thread-ctru.h b/firmware/target/hosted/game_console/thread-console.h similarity index 92% rename from firmware/target/hosted/ctru/thread-ctru.h rename to firmware/target/hosted/game_console/thread-console.h index 6f18576991..d9cd815a04 100644 --- a/firmware/target/hosted/ctru/thread-ctru.h +++ b/firmware/target/hosted/game_console/thread-console.h @@ -18,8 +18,8 @@ * ****************************************************************************/ -#ifndef __THREAD_CTRU_H__ -#define __THREAD_CTRU_H__ +#ifndef __THREAD_CONSOLE_H__ +#define __THREAD_CONSOLE_H__ /* extra thread functions that only apply when running on hosting platforms */ void sim_thread_lock(void *me); @@ -27,5 +27,5 @@ void * sim_thread_unlock(void); void sim_thread_exception_wait(void); void sim_thread_shutdown(void); /* Shut down all kernel threads gracefully */ -#endif /* #ifndef __THREAD_CTRU_H__ */ +#endif /* #ifndef __THREAD_CONSOLE_H__ */ diff --git a/firmware/target/hosted/ctru/timer-ctru.c b/firmware/target/hosted/game_console/timer-console.c similarity index 100% rename from firmware/target/hosted/ctru/timer-ctru.c rename to firmware/target/hosted/game_console/timer-console.c diff --git a/firmware/target/hosted/rtc.c b/firmware/target/hosted/rtc.c index 959b1cb8c1..9cb00b6422 100644 --- a/firmware/target/hosted/rtc.c +++ b/firmware/target/hosted/rtc.c @@ -23,7 +23,7 @@ #include #if !defined(WIN32) #include -#if !defined(__APPLE__) && !defined(CTRU) +#if !defined(__APPLE__) && !defined(N3DS) #include #endif #include @@ -48,7 +48,7 @@ int rtc_read_datetime(struct tm *tm) int rtc_write_datetime(const struct tm *tm) { -#if !defined(WIN32) && !defined(__APPLE__) && !defined(CTRU) +#if !defined(WIN32) && !defined(__APPLE__) && !defined(N3DS) struct timeval tv; struct tm *tm_time; diff --git a/lib/rbcodec/codecs/codec_crt0.c b/lib/rbcodec/codecs/codec_crt0.c index d3bde77339..831af0924b 100644 --- a/lib/rbcodec/codecs/codec_crt0.c +++ b/lib/rbcodec/codecs/codec_crt0.c @@ -30,7 +30,7 @@ extern unsigned char plugin_end_addr[]; * avoid warning with certain compilers */ int _start(void) {return 0;} -#ifdef CTRU +#ifdef N3DS /* dummy undefined symbols */ void __aeabi_unwind_cpp_pr0(void) {} struct _reent * _EXFUN(__getreent, (void)) { return NULL; } diff --git a/packaging/ctru/ctru.make b/packaging/3ds/3ds.make similarity index 93% rename from packaging/ctru/ctru.make rename to packaging/3ds/3ds.make index 838ff12b90..744bae8747 100644 --- a/packaging/ctru/ctru.make +++ b/packaging/3ds/3ds.make @@ -31,13 +31,13 @@ VERSION_MICRO := 0 APP_TITLE := rockbox APP_DESCRIPTION := Open Source Jukebox Firmware APP_AUTHOR := rockbox.org -APP_ICON := $(ROOTDIR)/packaging/ctru/res/icon.png +APP_ICON := $(ROOTDIR)/packaging/3ds/res/icon.png # CIA -BANNER_AUDIO := $(ROOTDIR)/packaging/ctru/res/banner.wav -BANNER_IMAGE := $(ROOTDIR)/packaging/ctru/res/banner.cgfx -RSF_PATH := $(ROOTDIR)/packaging/ctru/res/app.rsf -#LOGO := $(ROOTDIR)/packaging/ctru/logo.lz11 +BANNER_AUDIO := $(ROOTDIR)/packaging/3ds/res/banner.wav +BANNER_IMAGE := $(ROOTDIR)/packaging/3ds/res/banner.cgfx +RSF_PATH := $(ROOTDIR)/packaging/3ds/res/app.rsf +#LOGO := $(ROOTDIR)/packaging/3ds/logo.lz11 UNIQUE_ID := 0xCB001 PRODUCT_CODE := CTR-ROCKBOX ICON_FLAGS := nosavebackups,visible diff --git a/packaging/ctru/res/app.rsf b/packaging/3ds/res/app.rsf similarity index 100% rename from packaging/ctru/res/app.rsf rename to packaging/3ds/res/app.rsf diff --git a/packaging/ctru/res/banner.cgfx b/packaging/3ds/res/banner.cgfx similarity index 100% rename from packaging/ctru/res/banner.cgfx rename to packaging/3ds/res/banner.cgfx diff --git a/packaging/ctru/res/banner.png b/packaging/3ds/res/banner.png similarity index 100% rename from packaging/ctru/res/banner.png rename to packaging/3ds/res/banner.png diff --git a/packaging/ctru/res/banner.wav b/packaging/3ds/res/banner.wav similarity index 100% rename from packaging/ctru/res/banner.wav rename to packaging/3ds/res/banner.wav diff --git a/packaging/ctru/res/banner_.cgfx b/packaging/3ds/res/banner_.cgfx similarity index 100% rename from packaging/ctru/res/banner_.cgfx rename to packaging/3ds/res/banner_.cgfx diff --git a/packaging/ctru/res/icon.png b/packaging/3ds/res/icon.png similarity index 100% rename from packaging/ctru/res/icon.png rename to packaging/3ds/res/icon.png diff --git a/packaging/ctru/res/icon_yellow.png b/packaging/3ds/res/icon_yellow.png similarity index 100% rename from packaging/ctru/res/icon_yellow.png rename to packaging/3ds/res/icon_yellow.png diff --git a/packaging/ctru/rockbox.xml b/packaging/3ds/rockbox.xml similarity index 100% rename from packaging/ctru/rockbox.xml rename to packaging/3ds/rockbox.xml diff --git a/tools/buildzip.pl b/tools/buildzip.pl index 8f546db270..b8f9c12530 100755 --- a/tools/buildzip.pl +++ b/tools/buildzip.pl @@ -424,7 +424,7 @@ sub buildzip { glob_copy("$ROOT/firmware/drivers/libertas/firmware/*", "$temp_dir/libertas/"); } # add hbmenu shortcut's icon and 3dsx executable - if ($modelname =~ /ctru/) { + if ($modelname =~ /3ds/) { glob_copy("icon.icn", "$temp_dir/"); glob_copy("rockbox.3dsx", "$temp_dir/"); } @@ -755,12 +755,12 @@ sub runone { } # add hbmenu shortcut and cia file to zip file - if ($modelname =~ /ctru/) { + if ($modelname =~ /3ds/) { copy("rockbox.cia", "3ds"); - copy("$ROOT/packaging/ctru/rockbox.xml", "3ds"); + copy("$ROOT/packaging/3ds/rockbox.xml", "3ds"); system("$ziptool -u $output 3ds/rockbox.xml $target >/dev/null"); - print "$ziptool $output $ROOT/packaging/ctru/rockbox.xml $target >/dev/null\n" if $verbose; + print "$ziptool $output $ROOT/packaging/3ds/rockbox.xml $target >/dev/null\n" if $verbose; system("$ziptool -u $output 3ds/rockbox.cia $target >/dev/null"); print "$ziptool $output rockbox.cia $target >/dev/null\n" if $verbose; } diff --git a/tools/configure b/tools/configure index 08ceb746ba..b4063cd2f1 100755 --- a/tools/configure +++ b/tools/configure @@ -4154,12 +4154,12 @@ fi sysfontbl="16-Terminus" ;; - 290|ctru) + 290|3ds) target_id=122 application="yes" - modelname="ctru" - app_type="ctru-app" - target="CTRU" + modelname="3ds" + app_type="3ds-homebrew" + target="N3DS" memory=16 uname=`uname` devkitarmcc @@ -4172,8 +4172,8 @@ fi appextra="recorder:gui" plugins="yes" t_cpu="hosted" - t_manufacturer="ctru" - t_model="app" + t_manufacturer="game_console" + t_model="3ds" ;; 300|hibyr3proii) @@ -4850,9 +4850,9 @@ if test -n "$t_cpu"; then if [ "$simulator" = "yes" ]; then # a few more includes for the sim target tree TARGET_INC="$TARGET_INC -I\$(FIRMDIR)/target/hosted/sdl" TARGET_INC="$TARGET_INC -I\$(FIRMDIR)/target/hosted" - elif [ "$application" = "yes" ] && [ "$t_manufacturer" = "ctru" ]; then - TARGET_INC="$TARGET_INC -I\$(FIRMDIR)/target/hosted/ctru/lib" - TARGET_INC="$TARGET_INC -I\$(FIRMDIR)/target/hosted/ctru/lib/bfile" + elif [ "$application" = "yes" ] && [ "$t_manufacturer" = "game_console" ]; then + TARGET_INC="$TARGET_INC -I\$(FIRMDIR)/target/hosted/game_console/lib" + TARGET_INC="$TARGET_INC -I\$(FIRMDIR)/target/hosted/game_console/lib/bfile" fi if [ "$tinyalsa" = "yes" ]; then diff --git a/tools/root.make b/tools/root.make index 46acf85e78..878ecd1c36 100644 --- a/tools/root.make +++ b/tools/root.make @@ -176,8 +176,8 @@ else # core include $(ROOTDIR)/packaging/rgnano/rgnano.make endif - ifneq (,$(findstring ctru, $(APP_TYPE))) - include $(ROOTDIR)/packaging/ctru/ctru.make + ifneq (,$(findstring 3ds-homebrew, $(APP_TYPE))) + include $(ROOTDIR)/packaging/3ds/3ds.make endif endif # bootloader From 9d86c9b201bad1222b87364e45cfad630259fff3 Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Tue, 11 Aug 2026 21:28:46 +0100 Subject: [PATCH 137/408] sdmmc_host: reject commands the card reports as failed sdmmc_host looks only at the transport status the controller returns, and passes NULL for the response of every data transfer, so nothing ever reads R1. A card does not signal a rejected command by failing the transfer: it answers normally and simply does not commit the data. A write refused for a write-protect violation, an address error or an internal ECC failure is therefore reported to the filesystem as a success. Add sdmmc_host_submit_cmd_r1(), which fails a command whose response carries any bit in SD_R1_CARD_ERROR, and use it for SET_BLOCKLEN and the read or write itself. Those responses were already being received and discarded, so this costs no extra bus traffic. The R1 of the transfer command is returned before the data moves, so it cannot report a failure which happened during the transfer. Whatever ends the transfer has to be checked too: CMD12 where it terminates a multiblock transfer, and CMD13 (SEND_STATUS) where CMD23 was used and there is no closing command. Without the CMD13 an error is reported only on the next transfer, against the wrong sector. A CMD12-terminated read which ends on the last block of the card will have tried to read past the end, and the SD spec (4.3.3, "Block Read") requires the resulting OUT_OF_RANGE to be ignored. It is masked out for exactly that case. Only valid for R1 and R1b. A controller cannot apply the check itself because it is told the response length rather than its format, and R3, R6 and R7 are also 48-bit responses carrying unrelated bits in the same positions. Affects any target building sdmmc_host. Exercised on X1600 hardware over 8 GiB of sequential reads with no errors; the error path itself was not observed to trigger. Not run on X1000 hardware. Change-Id: I4a80dd29385d3eb4f256bc745a84f96e7054bbf8 Co-Authored-By: Claude Opus 5 --- firmware/drivers/sdmmc_host.c | 89 +++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/firmware/drivers/sdmmc_host.c b/firmware/drivers/sdmmc_host.c index aa95bee9ab..23c75175e3 100644 --- a/firmware/drivers/sdmmc_host.c +++ b/firmware/drivers/sdmmc_host.c @@ -325,6 +325,37 @@ static int sdmmc_host_submit_cmd(struct sdmmc_host *host, return host->ops->submit_command(host->controller, cmd, resp); } +/* + * Submit one command and check the card status in its R1 response. + * + * A card reports a rejected command in R1 rather than by failing the + * transfer: it answers normally, the controller sees nothing wrong, and + * the data is silently not committed. Only use this for commands whose + * response is R1 or R1b; R3, R6 and R7 carry unrelated bits in the same + * positions. + * + * Error bits set in ignore_mask are not treated as failures. + */ +static int sdmmc_host_submit_cmd_r1(struct sdmmc_host *host, + const struct sdmmc_host_command *cmd, + uint32_t ignore_mask) +{ + struct sdmmc_host_response resp; + + int rc = sdmmc_host_submit_cmd(host, cmd, &resp); + if (rc) + return rc; + + if (resp.data[0] & SD_R1_CARD_ERROR & ~ignore_mask) + { + logf("%s: cmd%d card status %08lx", __func__, (int)cmd->command, + (unsigned long)resp.data[0]); + return SDMMC_STATUS_ERROR; + } + + return SDMMC_STATUS_OK; +} + /* * Execute an SD APP_CMD */ @@ -589,7 +620,7 @@ static int sdmmc_host_cmd_set_block_len(struct sdmmc_host *host, int len) .flags = SDMMC_RESP_SHORT, }; - return sdmmc_host_submit_cmd(host, &cmd, NULL); + return sdmmc_host_submit_cmd_r1(host, &cmd, 0); } static int sdmmc_host_cmd_set_block_count(struct sdmmc_host *host, int count) @@ -614,6 +645,17 @@ static int sdmmc_host_cmd_set_block_count(struct sdmmc_host *host, int count) return rc; } +static int sdmmc_host_cmd_send_status(struct sdmmc_host *host) +{ + struct sdmmc_host_command cmd = { + .command = SD_SEND_STATUS, + .argument = host->cardinfo.rca, + .flags = SDMMC_RESP_SHORT, + }; + + return sdmmc_host_submit_cmd_r1(host, &cmd, 0); +} + static void sdmmc_host_set_controller_bus_width(struct sdmmc_host *host, uint32_t width) { if (host->ops->set_bus_width) @@ -852,7 +894,7 @@ static int sdmmc_host_transfer(struct sdmmc_host *host, else cmd.argument = start * SD_BLOCK_SIZE; - rc = sdmmc_host_submit_cmd(host, &cmd, NULL); + rc = sdmmc_host_submit_cmd_r1(host, &cmd, 0); if (rc == SDMMC_STATUS_TIMEOUT && !host->quirk_rdwrsingleblock_delay && @@ -874,17 +916,44 @@ static int sdmmc_host_transfer(struct sdmmc_host *host, goto out; /* - * NOTE: some controllers can send CMD12 automatically after - * the end of a transfer, eg. X1000; it might be worth - * supporting that via a feature flag. + * The R1 above was returned before the data moved, so it cannot + * report anything which went wrong during the transfer. Whatever + * ends the transfer has to be checked too, and a CMD23 transfer + * ends without a command. */ - if (xfer_count > 1 && !host->use_cmd23) + if (xfer_count > 1) { - memset(&cmd, 0, sizeof(cmd)); - cmd.command = SD_STOP_TRANSMISSION; - cmd.flags = SDMMC_RESP_SHORT | SDMMC_RESP_BUSY; + if (host->use_cmd23) + { + rc = sdmmc_host_cmd_send_status(host); + } + else + { + /* + * NOTE: some controllers can send CMD12 automatically + * after the end of a transfer, eg. X1000; it might + * be worth supporting that via a feature flag. + */ + uint32_t ignore = 0; + + /* + * A CMD12-terminated read runs until it is stopped, so + * one ending on the last block of the card will have + * tried to read past the end and set OUT_OF_RANGE. The + * SD spec (4.3.3, "Block Read") requires it to be + * ignored. + */ + if (data_dir == SDMMC_DATA_READ && + start + xfer_count == host->cardinfo.numblocks) + ignore = SD_R1_OUT_OF_RANGE; + + memset(&cmd, 0, sizeof(cmd)); + cmd.command = SD_STOP_TRANSMISSION; + cmd.flags = SDMMC_RESP_SHORT | SDMMC_RESP_BUSY; + + rc = sdmmc_host_submit_cmd_r1(host, &cmd, ignore); + } - rc = sdmmc_host_submit_cmd(host, &cmd, NULL); if (rc) goto out; } From f349e851544179368303a0fb69d9660fb5c876cb Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Mon, 10 Aug 2026 21:48:18 +0100 Subject: [PATCH 138/408] ingenic: move the drivers the X1000 shares with other SoCs Pure file move, in preparation for a second Ingenic SoC. dma, gpio, i2c, installer, kernel, nand and the SPL NAND backend move from target/mips/ingenic_x1000/ to target/mips/ingenic/ Drivers that cannot move as whole files, because part of each is genuinely X1000-specific, are left alone for now: msc, uart, sfc, the debug menu and the OST helpers in system-x1000.c. Verified as a no-op: built at target 246 --type=b before and after from the same source path, bootloader.bin is byte-identical, and every moved file's object has the same instruction stream under its new name. Builds clean with no warnings for targets 246, 260 and 247, both --type=n and --type=b. Change-Id: I9f67c23384df49b8c4554d695b772bcd54a4e32f Co-Authored-By: Claude Opus 5 --- bootloader/x1000/install.c | 2 +- bootloader/x1000/main.c | 2 +- bootloader/x1000/utils.c | 2 +- firmware/SOURCES | 18 +++++++++--------- firmware/drivers/audio/eros_qn_codec.c | 4 ++-- .../dma-x1000.c => ingenic/dma-ingenic.c} | 2 +- .../dma-x1000.h => ingenic/dma-ingenic.h} | 6 +++--- .../gpio-x1000.c => ingenic/gpio-ingenic.c} | 2 +- .../gpio-x1000.h => ingenic/gpio-ingenic.h} | 6 +++--- .../i2c-x1000.c => ingenic/i2c-ingenic.c} | 2 +- .../i2c-x1000.h => ingenic/i2c-ingenic.h} | 6 +++--- .../installer-ingenic.c} | 4 ++-- .../installer-ingenic.h} | 6 +++--- .../kernel-ingenic.c} | 0 .../nand-x1000.c => ingenic/nand-ingenic.c} | 2 +- .../nand-x1000.h => ingenic/nand-ingenic.h} | 6 +++--- .../spl-nand-ingenic.c} | 4 ++-- firmware/target/mips/ingenic_x1000/aic-x1000.c | 2 +- .../target/mips/ingenic_x1000/boot-x1000.c | 4 ++-- .../target/mips/ingenic_x1000/debug-x1000.c | 2 +- .../erosqnative/audiohw-erosqnative.c | 4 ++-- .../erosqnative/button-erosqnative.c | 4 ++-- .../erosqnative/lcd-erosqnative.c | 2 +- .../erosqnative/power-erosqnative.c | 2 +- .../ingenic_x1000/fiiom3k/audiohw-fiiom3k.c | 4 ++-- .../ingenic_x1000/fiiom3k/button-fiiom3k.c | 4 ++-- .../mips/ingenic_x1000/fiiom3k/lcd-fiiom3k.c | 2 +- .../mips/ingenic_x1000/fiiom3k/power-fiiom3k.c | 2 +- firmware/target/mips/ingenic_x1000/lcd-x1000.c | 2 +- firmware/target/mips/ingenic_x1000/msc-x1000.c | 2 +- firmware/target/mips/ingenic_x1000/pcm-x1000.c | 2 +- firmware/target/mips/ingenic_x1000/pwm-x1000.c | 2 +- .../mips/ingenic_x1000/sdmmc-x1000-common.c | 2 +- .../shanlingq1/audiohw-shanlingq1.c | 4 ++-- .../shanlingq1/button-shanlingq1.c | 4 ++-- .../ingenic_x1000/shanlingq1/lcd-shanlingq1.c | 2 +- .../shanlingq1/power-shanlingq1.c | 2 +- firmware/target/mips/ingenic_x1000/spl-x1000.c | 4 ++-- .../target/mips/ingenic_x1000/system-x1000.c | 4 ++-- .../target/mips/ingenic_x1000/uart-x1000.c | 2 +- firmware/target/mips/ingenic_x1000/usb-x1000.c | 2 +- tools/configure | 5 +++++ 42 files changed, 75 insertions(+), 70 deletions(-) rename firmware/target/mips/{ingenic_x1000/dma-x1000.c => ingenic/dma-ingenic.c} (99%) rename firmware/target/mips/{ingenic_x1000/dma-x1000.h => ingenic/dma-ingenic.h} (96%) rename firmware/target/mips/{ingenic_x1000/gpio-x1000.c => ingenic/gpio-ingenic.c} (99%) rename firmware/target/mips/{ingenic_x1000/gpio-x1000.h => ingenic/gpio-ingenic.h} (98%) rename firmware/target/mips/{ingenic_x1000/i2c-x1000.c => ingenic/i2c-ingenic.c} (99%) rename firmware/target/mips/{ingenic_x1000/i2c-x1000.h => ingenic/i2c-ingenic.h} (94%) rename firmware/target/mips/{ingenic_x1000/installer-x1000.c => ingenic/installer-ingenic.c} (99%) rename firmware/target/mips/{ingenic_x1000/installer-x1000.h => ingenic/installer-ingenic.h} (95%) rename firmware/target/mips/{ingenic_x1000/kernel-x1000.c => ingenic/kernel-ingenic.c} (100%) rename firmware/target/mips/{ingenic_x1000/nand-x1000.c => ingenic/nand-ingenic.c} (99%) rename firmware/target/mips/{ingenic_x1000/nand-x1000.h => ingenic/nand-ingenic.h} (99%) rename firmware/target/mips/{ingenic_x1000/spl-nand-x1000.c => ingenic/spl-nand-ingenic.c} (96%) diff --git a/bootloader/x1000/install.c b/bootloader/x1000/install.c index ffdbee709e..9259d9a545 100644 --- a/bootloader/x1000/install.c +++ b/bootloader/x1000/install.c @@ -21,7 +21,7 @@ #include "x1000bootloader.h" #include "kernel.h" #include "button.h" -#include "installer-x1000.h" +#include "installer-ingenic.h" #include enum { diff --git a/bootloader/x1000/main.c b/bootloader/x1000/main.c index 41009253cd..d479e886b0 100644 --- a/bootloader/x1000/main.c +++ b/bootloader/x1000/main.c @@ -28,7 +28,7 @@ #include "disk.h" #include "file_internal.h" #include "usb.h" -#include "i2c-x1000.h" +#include "i2c-ingenic.h" #include "boot-x1000.h" #include diff --git a/bootloader/x1000/utils.c b/bootloader/x1000/utils.c index 2cfde16ff9..c0ec69ac2f 100644 --- a/bootloader/x1000/utils.c +++ b/bootloader/x1000/utils.c @@ -29,7 +29,7 @@ #include "loader_strerror.h" #include "linuxboot.h" #include "screendump.h" -#include "nand-x1000.h" +#include "nand-ingenic.h" #include "sfc-x1000.h" /* Set to true if a SYS_USB_CONNECTED event is seen diff --git a/firmware/SOURCES b/firmware/SOURCES index f7bcd5d2b9..3a5f562bc9 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1759,12 +1759,12 @@ target/mips/ingenic_x1000/aic-x1000.c target/mips/ingenic_x1000/boot-x1000.c target/mips/ingenic_x1000/clk-x1000.c target/mips/ingenic_x1000/debug-x1000.c -target/mips/ingenic_x1000/dma-x1000.c -target/mips/ingenic_x1000/gpio-x1000.c -target/mips/ingenic_x1000/i2c-x1000.c -target/mips/ingenic_x1000/kernel-x1000.c +target/mips/ingenic/dma-ingenic.c +target/mips/ingenic/gpio-ingenic.c +target/mips/ingenic/i2c-ingenic.c +target/mips/ingenic/kernel-ingenic.c target/mips/ingenic_x1000/lcd-x1000.c -target/mips/ingenic_x1000/nand-x1000.c +target/mips/ingenic/nand-ingenic.c target/mips/ingenic_x1000/pcm-x1000.c target/mips/ingenic_x1000/pwm-x1000.c target/mips/ingenic_x1000/sfc-x1000.c @@ -1778,7 +1778,7 @@ target/mips/ingenic_x1000/usb-x1000.c target/mips/ingenic_x1000/msc-x1000.c #endif #ifdef BOOTLOADER -target/mips/ingenic_x1000/installer-x1000.c +target/mips/ingenic/installer-ingenic.c target/mips/ingenic_x1000/spl-start.S target/mips/ingenic_x1000/spl-x1000.c common/ucl_decompress.c @@ -1819,7 +1819,7 @@ target/mips/ingenic_x1000/fiiom3k/button-fiiom3k.c target/mips/ingenic_x1000/fiiom3k/lcd-fiiom3k.c target/mips/ingenic_x1000/fiiom3k/power-fiiom3k.c target/mips/ingenic_x1000/sdmmc-x1000-common.c -target/mips/ingenic_x1000/spl-nand-x1000.c +target/mips/ingenic/spl-nand-ingenic.c #endif /* FIIO_M3K */ #if defined(SHANLING_Q1) @@ -1829,7 +1829,7 @@ target/mips/ingenic_x1000/shanlingq1/button-shanlingq1.c target/mips/ingenic_x1000/shanlingq1/lcd-shanlingq1.c target/mips/ingenic_x1000/shanlingq1/power-shanlingq1.c target/mips/ingenic_x1000/sdmmc-x1000-common.c -target/mips/ingenic_x1000/spl-nand-x1000.c +target/mips/ingenic/spl-nand-ingenic.c #endif /* SHANLING_Q1 */ #if defined(EROS_QN) @@ -1839,7 +1839,7 @@ target/mips/ingenic_x1000/erosqnative/button-erosqnative.c target/mips/ingenic_x1000/erosqnative/lcd-erosqnative.c target/mips/ingenic_x1000/erosqnative/power-erosqnative.c target/mips/ingenic_x1000/sdmmc-x1000-common.c -target/mips/ingenic_x1000/spl-nand-x1000.c +target/mips/ingenic/spl-nand-ingenic.c #endif /* EROS_QN */ #if defined(SAMSUNG_YH820) || defined(SAMSUNG_YH920) || defined(SAMSUNG_YH925) diff --git a/firmware/drivers/audio/eros_qn_codec.c b/firmware/drivers/audio/eros_qn_codec.c index fc01f7fd09..2bbab3f3bd 100644 --- a/firmware/drivers/audio/eros_qn_codec.c +++ b/firmware/drivers/audio/eros_qn_codec.c @@ -31,8 +31,8 @@ // #define LOGF_ENABLE #include "logf.h" -#include "gpio-x1000.h" -#include "i2c-x1000.h" +#include "gpio-ingenic.h" +#include "i2c-ingenic.h" int es9018k2m_present_flag = 0; diff --git a/firmware/target/mips/ingenic_x1000/dma-x1000.c b/firmware/target/mips/ingenic/dma-ingenic.c similarity index 99% rename from firmware/target/mips/ingenic_x1000/dma-x1000.c rename to firmware/target/mips/ingenic/dma-ingenic.c index f3b68494fb..8a6a0e3361 100644 --- a/firmware/target/mips/ingenic_x1000/dma-x1000.c +++ b/firmware/target/mips/ingenic/dma-ingenic.c @@ -19,7 +19,7 @@ ****************************************************************************/ #include "system.h" -#include "dma-x1000.h" +#include "dma-ingenic.h" #include "irq-x1000.h" #include "x1000/cpm.h" #include "panic.h" diff --git a/firmware/target/mips/ingenic_x1000/dma-x1000.h b/firmware/target/mips/ingenic/dma-ingenic.h similarity index 96% rename from firmware/target/mips/ingenic_x1000/dma-x1000.h rename to firmware/target/mips/ingenic/dma-ingenic.h index 514d6e3097..15d7fda120 100644 --- a/firmware/target/mips/ingenic_x1000/dma-x1000.h +++ b/firmware/target/mips/ingenic/dma-ingenic.h @@ -18,8 +18,8 @@ * ****************************************************************************/ -#ifndef __DMA_X1000_H__ -#define __DMA_X1000_H__ +#ifndef __DMA_INGENIC_H__ +#define __DMA_INGENIC_H__ #include "x1000/dma.h" #include "x1000/dma_chn.h" @@ -66,4 +66,4 @@ typedef void(*dma_cb_func)(int event); extern void dma_init(void) INIT_ATTR; extern void dma_set_callback(int chn, dma_cb_func cb); -#endif /* __DMA_X1000_H__ */ +#endif /* __DMA_INGENIC_H__ */ diff --git a/firmware/target/mips/ingenic_x1000/gpio-x1000.c b/firmware/target/mips/ingenic/gpio-ingenic.c similarity index 99% rename from firmware/target/mips/ingenic_x1000/gpio-x1000.c rename to firmware/target/mips/ingenic/gpio-ingenic.c index b67eb1739c..61a46b7cb9 100644 --- a/firmware/target/mips/ingenic_x1000/gpio-x1000.c +++ b/firmware/target/mips/ingenic/gpio-ingenic.c @@ -18,7 +18,7 @@ * ****************************************************************************/ -#include "gpio-x1000.h" +#include "gpio-ingenic.h" #if defined(EROS_QN) # include "devicedata.h" #endif diff --git a/firmware/target/mips/ingenic_x1000/gpio-x1000.h b/firmware/target/mips/ingenic/gpio-ingenic.h similarity index 98% rename from firmware/target/mips/ingenic_x1000/gpio-x1000.h rename to firmware/target/mips/ingenic/gpio-ingenic.h index 3798027cf8..197c1f86f3 100644 --- a/firmware/target/mips/ingenic_x1000/gpio-x1000.h +++ b/firmware/target/mips/ingenic/gpio-ingenic.h @@ -18,8 +18,8 @@ * ****************************************************************************/ -#ifndef __GPIO_X1000_H__ -#define __GPIO_X1000_H__ +#ifndef __GPIO_INGENIC_H__ +#define __GPIO_INGENIC_H__ #include "x1000/gpio.h" #include "config.h" @@ -167,4 +167,4 @@ static inline void gpio_flip_edge_irq(int gpio) gpio_set_irq_level(gpio, 1); } -#endif /* __GPIO_X1000_H__ */ +#endif /* __GPIO_INGENIC_H__ */ diff --git a/firmware/target/mips/ingenic_x1000/i2c-x1000.c b/firmware/target/mips/ingenic/i2c-ingenic.c similarity index 99% rename from firmware/target/mips/ingenic_x1000/i2c-x1000.c rename to firmware/target/mips/ingenic/i2c-ingenic.c index c73f78fb2a..0abf763aa7 100644 --- a/firmware/target/mips/ingenic_x1000/i2c-x1000.c +++ b/firmware/target/mips/ingenic/i2c-ingenic.c @@ -19,7 +19,7 @@ ****************************************************************************/ /* #define LOGF_ENABLE */ -#include "i2c-x1000.h" +#include "i2c-ingenic.h" #include "system.h" #include "kernel.h" #include "panic.h" diff --git a/firmware/target/mips/ingenic_x1000/i2c-x1000.h b/firmware/target/mips/ingenic/i2c-ingenic.h similarity index 94% rename from firmware/target/mips/ingenic_x1000/i2c-x1000.h rename to firmware/target/mips/ingenic/i2c-ingenic.h index 12ac5465d4..f0d5077c39 100644 --- a/firmware/target/mips/ingenic_x1000/i2c-x1000.h +++ b/firmware/target/mips/ingenic/i2c-ingenic.h @@ -18,8 +18,8 @@ * ****************************************************************************/ -#ifndef __I2C_X1000_H__ -#define __I2C_X1000_H__ +#ifndef __I2C_INGENIC_H__ +#define __I2C_INGENIC_H__ #include "i2c-async.h" @@ -40,4 +40,4 @@ extern void i2c_init(void) INIT_ATTR; */ extern void i2c_x1000_set_freq(int chn, int freq); -#endif /* __I2C_X1000_H__ */ +#endif /* __I2C_INGENIC_H__ */ diff --git a/firmware/target/mips/ingenic_x1000/installer-x1000.c b/firmware/target/mips/ingenic/installer-ingenic.c similarity index 99% rename from firmware/target/mips/ingenic_x1000/installer-x1000.c rename to firmware/target/mips/ingenic/installer-ingenic.c index 1b9ecf1f02..2aba947758 100644 --- a/firmware/target/mips/ingenic_x1000/installer-x1000.c +++ b/firmware/target/mips/ingenic/installer-ingenic.c @@ -18,8 +18,8 @@ * ****************************************************************************/ -#include "installer-x1000.h" -#include "nand-x1000.h" +#include "installer-ingenic.h" +#include "nand-ingenic.h" #include "core_alloc.h" #include "file.h" #include "microtar-rockbox.h" diff --git a/firmware/target/mips/ingenic_x1000/installer-x1000.h b/firmware/target/mips/ingenic/installer-ingenic.h similarity index 95% rename from firmware/target/mips/ingenic_x1000/installer-x1000.h rename to firmware/target/mips/ingenic/installer-ingenic.h index 47bc7d7bfb..416db57906 100644 --- a/firmware/target/mips/ingenic_x1000/installer-x1000.h +++ b/firmware/target/mips/ingenic/installer-ingenic.h @@ -18,8 +18,8 @@ * ****************************************************************************/ -#ifndef __INSTALLER_X1000_H__ -#define __INSTALLER_X1000_H__ +#ifndef __INSTALLER_INGENIC_H__ +#define __INSTALLER_INGENIC_H__ /* This API is for the bootloader recovery menu and Rockbox utility to handle * bootloader installation, backup, and restore. @@ -52,4 +52,4 @@ extern int backup_bootloader(const char* filename); extern int restore_bootloader(const char* filename); extern const char* installer_strerror(int rc); -#endif /* __INSTALLER_X1000_H__ */ +#endif /* __INSTALLER_INGENIC_H__ */ diff --git a/firmware/target/mips/ingenic_x1000/kernel-x1000.c b/firmware/target/mips/ingenic/kernel-ingenic.c similarity index 100% rename from firmware/target/mips/ingenic_x1000/kernel-x1000.c rename to firmware/target/mips/ingenic/kernel-ingenic.c diff --git a/firmware/target/mips/ingenic_x1000/nand-x1000.c b/firmware/target/mips/ingenic/nand-ingenic.c similarity index 99% rename from firmware/target/mips/ingenic_x1000/nand-x1000.c rename to firmware/target/mips/ingenic/nand-ingenic.c index 6961cbe58a..67f0f17f4d 100644 --- a/firmware/target/mips/ingenic_x1000/nand-x1000.c +++ b/firmware/target/mips/ingenic/nand-ingenic.c @@ -18,7 +18,7 @@ * ****************************************************************************/ -#include "nand-x1000.h" +#include "nand-ingenic.h" #include "sfc-x1000.h" #include "system.h" #include "logf.h" diff --git a/firmware/target/mips/ingenic_x1000/nand-x1000.h b/firmware/target/mips/ingenic/nand-ingenic.h similarity index 99% rename from firmware/target/mips/ingenic_x1000/nand-x1000.h rename to firmware/target/mips/ingenic/nand-ingenic.h index c0796528c7..b330ea8a92 100644 --- a/firmware/target/mips/ingenic_x1000/nand-x1000.h +++ b/firmware/target/mips/ingenic/nand-ingenic.h @@ -18,8 +18,8 @@ * ****************************************************************************/ -#ifndef __NAND_X1000_H__ -#define __NAND_X1000_H__ +#ifndef __NAND_INGENIC_H__ +#define __NAND_INGENIC_H__ #include #include @@ -238,4 +238,4 @@ extern int nand_page_read(struct nand_drv* drv, nand_page_t page, void* buffer); extern int nand_read_bytes(struct nand_drv* drv, uint32_t byte_addr, uint32_t byte_len, void* buffer); extern int nand_write_bytes(struct nand_drv* drv, uint32_t byte_addr, uint32_t byte_len, const void* buffer); -#endif /* __NAND_X1000_H__ */ +#endif /* __NAND_INGENIC_H__ */ diff --git a/firmware/target/mips/ingenic_x1000/spl-nand-x1000.c b/firmware/target/mips/ingenic/spl-nand-ingenic.c similarity index 96% rename from firmware/target/mips/ingenic_x1000/spl-nand-x1000.c rename to firmware/target/mips/ingenic/spl-nand-ingenic.c index b9e5b53336..9af5edaece 100644 --- a/firmware/target/mips/ingenic_x1000/spl-nand-x1000.c +++ b/firmware/target/mips/ingenic/spl-nand-ingenic.c @@ -19,8 +19,8 @@ ****************************************************************************/ #include "spl-x1000.h" -#include "gpio-x1000.h" -#include "nand-x1000.h" +#include "gpio-ingenic.h" +#include "nand-ingenic.h" static struct nand_drv* ndrv = NULL; diff --git a/firmware/target/mips/ingenic_x1000/aic-x1000.c b/firmware/target/mips/ingenic_x1000/aic-x1000.c index f00239f285..ee678d2235 100644 --- a/firmware/target/mips/ingenic_x1000/aic-x1000.c +++ b/firmware/target/mips/ingenic_x1000/aic-x1000.c @@ -20,7 +20,7 @@ #include "system.h" #include "aic-x1000.h" -#include "gpio-x1000.h" +#include "gpio-ingenic.h" #include "x1000/aic.h" #include "x1000/cpm.h" diff --git a/firmware/target/mips/ingenic_x1000/boot-x1000.c b/firmware/target/mips/ingenic_x1000/boot-x1000.c index 078541054c..8fc73ede0d 100644 --- a/firmware/target/mips/ingenic_x1000/boot-x1000.c +++ b/firmware/target/mips/ingenic_x1000/boot-x1000.c @@ -21,8 +21,8 @@ #include "system.h" #include "usb.h" #include "boot-x1000.h" -#include "nand-x1000.h" -#include "gpio-x1000.h" +#include "nand-ingenic.h" +#include "gpio-ingenic.h" #include "clk-x1000.h" #include "x1000/cpm.h" #include "x1000/lcd.h" diff --git a/firmware/target/mips/ingenic_x1000/debug-x1000.c b/firmware/target/mips/ingenic_x1000/debug-x1000.c index dae9d675a8..2e2975a29d 100644 --- a/firmware/target/mips/ingenic_x1000/debug-x1000.c +++ b/firmware/target/mips/ingenic_x1000/debug-x1000.c @@ -28,7 +28,7 @@ #include "list.h" #include "clk-x1000.h" -#include "gpio-x1000.h" +#include "gpio-ingenic.h" static bool dbg_clocks(void) { diff --git a/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c b/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c index b80ef55d12..02475fdc2d 100644 --- a/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c +++ b/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c @@ -30,8 +30,8 @@ #include "logf.h" #include "aic-x1000.h" -#include "i2c-x1000.h" -#include "gpio-x1000.h" +#include "i2c-ingenic.h" +#include "gpio-ingenic.h" #include "devicedata.h" /* diff --git a/firmware/target/mips/ingenic_x1000/erosqnative/button-erosqnative.c b/firmware/target/mips/ingenic_x1000/erosqnative/button-erosqnative.c index eeaff810d4..f666513503 100644 --- a/firmware/target/mips/ingenic_x1000/erosqnative/button-erosqnative.c +++ b/firmware/target/mips/ingenic_x1000/erosqnative/button-erosqnative.c @@ -24,9 +24,9 @@ #include "powermgmt.h" #include "panic.h" #include "axp-pmu.h" -#include "gpio-x1000.h" +#include "gpio-ingenic.h" #include "irq-x1000.h" -#include "i2c-x1000.h" +#include "i2c-ingenic.h" #include "eros_qn_codec.h" #include #include diff --git a/firmware/target/mips/ingenic_x1000/erosqnative/lcd-erosqnative.c b/firmware/target/mips/ingenic_x1000/erosqnative/lcd-erosqnative.c index e19eefe275..73bbb9437c 100644 --- a/firmware/target/mips/ingenic_x1000/erosqnative/lcd-erosqnative.c +++ b/firmware/target/mips/ingenic_x1000/erosqnative/lcd-erosqnative.c @@ -21,7 +21,7 @@ #include "lcd.h" #include "kernel.h" #include "lcd-x1000.h" -#include "gpio-x1000.h" +#include "gpio-ingenic.h" #include "system.h" #include "devicedata.h" diff --git a/firmware/target/mips/ingenic_x1000/erosqnative/power-erosqnative.c b/firmware/target/mips/ingenic_x1000/erosqnative/power-erosqnative.c index 6b9395c119..04e6572bb1 100644 --- a/firmware/target/mips/ingenic_x1000/erosqnative/power-erosqnative.c +++ b/firmware/target/mips/ingenic_x1000/erosqnative/power-erosqnative.c @@ -29,7 +29,7 @@ #endif #include "axp-pmu.h" #include "axp-2101.h" -#include "i2c-x1000.h" +#include "i2c-ingenic.h" #include "devicedata.h" unsigned short battery_level_disksafe = 3470; diff --git a/firmware/target/mips/ingenic_x1000/fiiom3k/audiohw-fiiom3k.c b/firmware/target/mips/ingenic_x1000/fiiom3k/audiohw-fiiom3k.c index 3e2b2fa204..5bb8420889 100644 --- a/firmware/target/mips/ingenic_x1000/fiiom3k/audiohw-fiiom3k.c +++ b/firmware/target/mips/ingenic_x1000/fiiom3k/audiohw-fiiom3k.c @@ -23,8 +23,8 @@ #include "system.h" #include "pcm_sampr.h" #include "aic-x1000.h" -#include "i2c-x1000.h" -#include "gpio-x1000.h" +#include "i2c-ingenic.h" +#include "gpio-ingenic.h" #include "logf.h" static int cur_audio_source = AUDIO_SRC_PLAYBACK; diff --git a/firmware/target/mips/ingenic_x1000/fiiom3k/button-fiiom3k.c b/firmware/target/mips/ingenic_x1000/fiiom3k/button-fiiom3k.c index e27fa08d36..fa18b4b109 100644 --- a/firmware/target/mips/ingenic_x1000/fiiom3k/button-fiiom3k.c +++ b/firmware/target/mips/ingenic_x1000/fiiom3k/button-fiiom3k.c @@ -25,9 +25,9 @@ #include "panic.h" #include "axp-pmu.h" #include "ft6x06.h" -#include "gpio-x1000.h" +#include "gpio-ingenic.h" #include "irq-x1000.h" -#include "i2c-x1000.h" +#include "i2c-ingenic.h" #include #include diff --git a/firmware/target/mips/ingenic_x1000/fiiom3k/lcd-fiiom3k.c b/firmware/target/mips/ingenic_x1000/fiiom3k/lcd-fiiom3k.c index a4f051bd27..3b6772c616 100644 --- a/firmware/target/mips/ingenic_x1000/fiiom3k/lcd-fiiom3k.c +++ b/firmware/target/mips/ingenic_x1000/fiiom3k/lcd-fiiom3k.c @@ -21,7 +21,7 @@ #include "lcd.h" #include "kernel.h" #include "lcd-x1000.h" -#include "gpio-x1000.h" +#include "gpio-ingenic.h" #include "system.h" static const uint32_t fiio_lcd_cmd_enable[] = { diff --git a/firmware/target/mips/ingenic_x1000/fiiom3k/power-fiiom3k.c b/firmware/target/mips/ingenic_x1000/fiiom3k/power-fiiom3k.c index a992c3de45..210a1ede2b 100644 --- a/firmware/target/mips/ingenic_x1000/fiiom3k/power-fiiom3k.c +++ b/firmware/target/mips/ingenic_x1000/fiiom3k/power-fiiom3k.c @@ -26,7 +26,7 @@ # include "usb_core.h" #endif #include "axp-pmu.h" -#include "i2c-x1000.h" +#include "i2c-ingenic.h" unsigned short battery_level_disksafe = 3470; diff --git a/firmware/target/mips/ingenic_x1000/lcd-x1000.c b/firmware/target/mips/ingenic_x1000/lcd-x1000.c index e8139d377d..754d8542d6 100644 --- a/firmware/target/mips/ingenic_x1000/lcd-x1000.c +++ b/firmware/target/mips/ingenic_x1000/lcd-x1000.c @@ -22,7 +22,7 @@ #include "system.h" #include "kernel.h" #include "lcd-x1000.h" -#include "dma-x1000.h" +#include "dma-ingenic.h" #include "irq-x1000.h" #include "x1000/lcd.h" #include "x1000/cpm.h" diff --git a/firmware/target/mips/ingenic_x1000/msc-x1000.c b/firmware/target/mips/ingenic_x1000/msc-x1000.c index 37ea53340d..cf79de2d03 100644 --- a/firmware/target/mips/ingenic_x1000/msc-x1000.c +++ b/firmware/target/mips/ingenic_x1000/msc-x1000.c @@ -21,7 +21,7 @@ #include "panic.h" #include "led.h" #include "msc-x1000.h" -#include "gpio-x1000.h" +#include "gpio-ingenic.h" #include "irq-x1000.h" #include "clk-x1000.h" #include "x1000/msc.h" diff --git a/firmware/target/mips/ingenic_x1000/pcm-x1000.c b/firmware/target/mips/ingenic_x1000/pcm-x1000.c index 3fb616c7e9..f877416aec 100644 --- a/firmware/target/mips/ingenic_x1000/pcm-x1000.c +++ b/firmware/target/mips/ingenic_x1000/pcm-x1000.c @@ -26,7 +26,7 @@ #include "pcm-internal.h" #include "pcm_sink.h" #include "panic.h" -#include "dma-x1000.h" +#include "dma-ingenic.h" #include "irq-x1000.h" #include "x1000/aic.h" #include "x1000/cpm.h" diff --git a/firmware/target/mips/ingenic_x1000/pwm-x1000.c b/firmware/target/mips/ingenic_x1000/pwm-x1000.c index 8a111b484e..99b12d2a6c 100644 --- a/firmware/target/mips/ingenic_x1000/pwm-x1000.c +++ b/firmware/target/mips/ingenic_x1000/pwm-x1000.c @@ -20,7 +20,7 @@ #include "pwm-x1000.h" #include "clk-x1000.h" -#include "gpio-x1000.h" +#include "gpio-ingenic.h" #include "system.h" #include "kernel.h" #include "x1000/tcu.h" diff --git a/firmware/target/mips/ingenic_x1000/sdmmc-x1000-common.c b/firmware/target/mips/ingenic_x1000/sdmmc-x1000-common.c index ac00a68369..9e3d4bc60e 100644 --- a/firmware/target/mips/ingenic_x1000/sdmmc-x1000-common.c +++ b/firmware/target/mips/ingenic_x1000/sdmmc-x1000-common.c @@ -20,7 +20,7 @@ #include "sdmmc_host.h" #include "sdmmc_poll.h" #include "msc-x1000.h" -#include "gpio-x1000.h" +#include "gpio-ingenic.h" #include "irq-x1000.h" #include "clk-x1000.h" diff --git a/firmware/target/mips/ingenic_x1000/shanlingq1/audiohw-shanlingq1.c b/firmware/target/mips/ingenic_x1000/shanlingq1/audiohw-shanlingq1.c index b55208c05f..518f5d9f3f 100644 --- a/firmware/target/mips/ingenic_x1000/shanlingq1/audiohw-shanlingq1.c +++ b/firmware/target/mips/ingenic_x1000/shanlingq1/audiohw-shanlingq1.c @@ -22,8 +22,8 @@ #include "system.h" #include "pcm_sampr.h" #include "aic-x1000.h" -#include "i2c-x1000.h" -#include "gpio-x1000.h" +#include "i2c-ingenic.h" +#include "gpio-ingenic.h" #include "x1000/aic.h" #include "x1000/cpm.h" diff --git a/firmware/target/mips/ingenic_x1000/shanlingq1/button-shanlingq1.c b/firmware/target/mips/ingenic_x1000/shanlingq1/button-shanlingq1.c index 66806bc013..1b06481c01 100644 --- a/firmware/target/mips/ingenic_x1000/shanlingq1/button-shanlingq1.c +++ b/firmware/target/mips/ingenic_x1000/shanlingq1/button-shanlingq1.c @@ -26,9 +26,9 @@ #include "kernel.h" #include "backlight.h" #include "powermgmt.h" -#include "gpio-x1000.h" +#include "gpio-ingenic.h" #include "irq-x1000.h" -#include "i2c-x1000.h" +#include "i2c-ingenic.h" #include #ifndef BOOTLOADER diff --git a/firmware/target/mips/ingenic_x1000/shanlingq1/lcd-shanlingq1.c b/firmware/target/mips/ingenic_x1000/shanlingq1/lcd-shanlingq1.c index 4cb72ceef0..f2830919d1 100644 --- a/firmware/target/mips/ingenic_x1000/shanlingq1/lcd-shanlingq1.c +++ b/firmware/target/mips/ingenic_x1000/shanlingq1/lcd-shanlingq1.c @@ -21,7 +21,7 @@ #include "lcd.h" #include "system.h" #include "lcd-x1000.h" -#include "gpio-x1000.h" +#include "gpio-ingenic.h" /* LCD controller is probably an RM68090. */ diff --git a/firmware/target/mips/ingenic_x1000/shanlingq1/power-shanlingq1.c b/firmware/target/mips/ingenic_x1000/shanlingq1/power-shanlingq1.c index 1a3a2bc164..4efd57857b 100644 --- a/firmware/target/mips/ingenic_x1000/shanlingq1/power-shanlingq1.c +++ b/firmware/target/mips/ingenic_x1000/shanlingq1/power-shanlingq1.c @@ -29,7 +29,7 @@ # include "usb_core.h" #endif -#include "i2c-x1000.h" +#include "i2c-ingenic.h" /* TODO: Better(?) battery reporting for Q1 using CW2015 driver * diff --git a/firmware/target/mips/ingenic_x1000/spl-x1000.c b/firmware/target/mips/ingenic_x1000/spl-x1000.c index fd8a49e14d..4461be33b9 100644 --- a/firmware/target/mips/ingenic_x1000/spl-x1000.c +++ b/firmware/target/mips/ingenic_x1000/spl-x1000.c @@ -21,8 +21,8 @@ #include "system.h" #include "spl-x1000.h" #include "clk-x1000.h" -#include "nand-x1000.h" -#include "gpio-x1000.h" +#include "nand-ingenic.h" +#include "gpio-ingenic.h" #include "boot-x1000.h" #include "x1000/cpm.h" #include "x1000/ost.h" diff --git a/firmware/target/mips/ingenic_x1000/system-x1000.c b/firmware/target/mips/ingenic_x1000/system-x1000.c index adc6ecf924..ec94c7236f 100644 --- a/firmware/target/mips/ingenic_x1000/system-x1000.c +++ b/firmware/target/mips/ingenic_x1000/system-x1000.c @@ -23,8 +23,8 @@ #include "mips.h" #include "panic.h" #include "button.h" -#include "gpio-x1000.h" -#include "dma-x1000.h" +#include "gpio-ingenic.h" +#include "dma-ingenic.h" #include "irq-x1000.h" #include "clk-x1000.h" #include "boot-x1000.h" diff --git a/firmware/target/mips/ingenic_x1000/uart-x1000.c b/firmware/target/mips/ingenic_x1000/uart-x1000.c index fd8ea9e4b7..e298bce8a6 100644 --- a/firmware/target/mips/ingenic_x1000/uart-x1000.c +++ b/firmware/target/mips/ingenic_x1000/uart-x1000.c @@ -18,7 +18,7 @@ * ****************************************************************************/ -#include "gpio-x1000.h" +#include "gpio-ingenic.h" #include "irq-x1000.h" #include "kernel.h" #include "panic.h" diff --git a/firmware/target/mips/ingenic_x1000/usb-x1000.c b/firmware/target/mips/ingenic_x1000/usb-x1000.c index 2c82b47cd5..d933813bf0 100644 --- a/firmware/target/mips/ingenic_x1000/usb-x1000.c +++ b/firmware/target/mips/ingenic_x1000/usb-x1000.c @@ -24,7 +24,7 @@ #include "usb_drv.h" #include "usb-designware.h" #include "irq-x1000.h" -#include "gpio-x1000.h" +#include "gpio-ingenic.h" #include "x1000/cpm.h" /* diff --git a/tools/configure b/tools/configure index b4063cd2f1..7f3628b540 100755 --- a/tools/configure +++ b/tools/configure @@ -3920,6 +3920,7 @@ fi # architecture, manufacturer and model for the target-tree build t_cpu="mips" t_manufacturer="ingenic_x1000" + t_soc="ingenic" t_model="fiiom3k" ;; @@ -3944,6 +3945,7 @@ fi # architecture, manufacturer and model for the target-tree build t_cpu="mips" t_manufacturer="ingenic_x1000" + t_soc="ingenic" t_model="erosqnative" # player version, for bootloader usage # versions 1 and 2 both use 1 @@ -3972,6 +3974,7 @@ fi # architecture, manufacturer and model for the target-tree build t_cpu="mips" t_manufacturer="ingenic_x1000" + t_soc="ingenic" t_model="erosqnative" # player version, for bootloader usage # version 3 @@ -4001,6 +4004,7 @@ fi # architecture, manufacturer and model for the target-tree build t_cpu="mips" t_manufacturer="ingenic_x1000" + t_soc="ingenic" t_model="erosqnative" # player version, for bootloader usage # version 4 @@ -4095,6 +4099,7 @@ fi # architecture, manufacturer and model for the target-tree build t_cpu="mips" t_manufacturer="ingenic_x1000" + t_soc="ingenic" t_model="shanlingq1" ;; From a3e93c496effb4b29242ffc1ad57d15c97d9cb07 Mon Sep 17 00:00:00 2001 From: Marc Andersen Date: Tue, 1 Sep 2026 13:42:09 +0200 Subject: [PATCH 139/408] lua: only define strerror/strcoll on native targets apps/plugins/lua/rockaux.c defines strerror() and strcoll() so the Lua plugin has something to link against on native targets, which have no libc providing them. Hosted targets do have a libc, and defining them there is at best redundant. On the Windows simulator it is worse than redundant. Both come from mingw's libmsvcrt.a, and because that is a static archive the linker pulls in an archive member for an unrelated symbol, then finds a second definition of these two, so lua.rock fails to link: libmsvcrt.a(...): multiple definition of `strcoll'; rockaux.o:rockaux.c:244: first defined here libmsvcrt.a(...): multiple definition of `strerror'; rockaux.o:rockaux.c:47: first defined here On hosted Linux there is no diagnostic, because glibc supplies these from a shared library where a local definition simply wins. That means the plugin has been quietly shadowing glibc's strerror() with a stub that always returned NULL, which is presumably not intended either. Guard both with CONFIG_PLATFORM & PLATFORM_NATIVE, matching the guard used a few lines above for errno. Hosted targets now get the real implementations from their own libc. This also silences a "redeclared without dllimport attribute" warning that GCC 16 emits for the same clash. Verified by building xduoox3 as a Windows simulator, xduoox3 as a Linux simulator, and sansaclip as a native ARM target: all three link cleanly with no multiple-definition errors. Provenance, per the AI disclosure requirement in docs/CONTRIBUTING: this change was drafted with Claude Code (Anthropic Claude Opus 5) at my direction. Patch set 2 adopts Aidan MacDonald's review suggestion to key the guard off PLATFORM_NATIVE rather than _WIN32. Change-Id: I8ffb6fd792147c9067afa708003b1285fb9d07a4 --- apps/plugins/lua/rockaux.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/plugins/lua/rockaux.c b/apps/plugins/lua/rockaux.c index 725e9c1d14..dd99ed1289 100644 --- a/apps/plugins/lua/rockaux.c +++ b/apps/plugins/lua/rockaux.c @@ -43,6 +43,7 @@ extern const char *strpbrk_n(const char *s, int smax, const char *accept); int errno = 0; #endif +#if (CONFIG_PLATFORM & PLATFORM_NATIVE) /* hosted platforms supply these */ char *strerror(int errnum) { (void) errnum; @@ -51,6 +52,7 @@ char *strerror(int errnum) return NULL; } +#endif /* splash string and allow user to scroll around * provides rudimentary text reflow @@ -239,10 +241,12 @@ long rb_pow(long x, long n) return pow; } +#if (CONFIG_PLATFORM & PLATFORM_NATIVE) /* hosted platforms supply these */ int strcoll(const char * str1, const char * str2) { return strcmp(str1, str2); } +#endif #if 0 //ndef _WIN32 /* supplied by strfrtime.lua*/ struct tm * gmtime(const time_t *timep) From 387b36fab737c7661becf2deef7cac853ec74cb7 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Sat, 15 Aug 2026 18:10:44 +0100 Subject: [PATCH 140/408] erosqnative: document the sample depth choice for es9018k2m The reason 16/24-bit on the DAC does not work is that this configures the I2S frame length, and the X1000's AIC uses a hardcoded 64-bit frame length regardless of the bit depth selected at the AIC input side. The AIC will pad the lower bits of the 32-bit output sample with zeros. Change-Id: I48c9893ca358248a63f24e3f75149da988943953 --- .../erosqnative/audiohw-erosqnative.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c b/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c index 02475fdc2d..9cb91bc55a 100644 --- a/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c +++ b/firmware/target/mips/ingenic_x1000/erosqnative/audiohw-erosqnative.c @@ -105,11 +105,17 @@ void audiohw_init(void) es9018k2m_present_flag = true; builtin_pcm_sink.caps.volume_type = PCM_SINK_HWVOL; - /* ES9018K2M defaults to 32-bit sample depth, which appears to work - properly with both 16-bit and 24-bit AIC configuration! Enabling - the following (ie to match the actual AIC configuration) produces - garbled output! */ - // es9018k2m_write_reg(ES9018K2M_REG1_INPUT_CONFIG, 0b00001100); // 16-bit data + /* + * Set sample depth to 32-bit. The sample depth setting corresponds + * to the I2S frame length which is twice the sample depth (since + * I2S is stereo). The AIC only supports 64-bit frames (see 6.3.1 + * "6.3.1 I2S and MSB-justified serial audio format" in the PM). + * + * Commented out since this is the hardware default. We technically + * do not need SPDIF/DSD detection so the lower nibble could be 0, + * but it was historically enabled does not seem to cause any harm. + */ + //es92018k2m_write_reg(ES9018K2M_REG1_INPUT_CONFIG, 0xcc); /* Datasheet: Sets the number os FSR edges that must occur before * * the DPLL and ASRC can lock on to the the incoming Signal. * From 94d422f1c4b4f8b79c3554d869e4b88f7290e748 Mon Sep 17 00:00:00 2001 From: Mauricio Garrido Date: Tue, 1 Sep 2026 16:40:27 -0600 Subject: [PATCH 141/408] 3ds: small fix. Rename missing ctru variable to 3ds and remove 3ds specific headers from common ones. Change-Id: Ib99cf2d31671dec0f6ca1de68ca2e33693991dc3 --- firmware/target/hosted/game_console/lib/sys_timer.h | 1 - firmware/target/hosted/game_console/system-console.h | 2 -- tools/functions.make | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/firmware/target/hosted/game_console/lib/sys_timer.h b/firmware/target/hosted/game_console/lib/sys_timer.h index 302fc216a2..564ec1bf4a 100644 --- a/firmware/target/hosted/game_console/lib/sys_timer.h +++ b/firmware/target/hosted/game_console/lib/sys_timer.h @@ -21,7 +21,6 @@ #ifndef __SYSTIMER_H__ #define __SYSTIMER_H__ -#include <3ds/svc.h> #include "sys_thread.h" typedef u32 (* timer_callback_ptr) (u32 interval, void *param); diff --git a/firmware/target/hosted/game_console/system-console.h b/firmware/target/hosted/game_console/system-console.h index 380c671d90..fe11b74445 100644 --- a/firmware/target/hosted/game_console/system-console.h +++ b/firmware/target/hosted/game_console/system-console.h @@ -23,8 +23,6 @@ #include #include -#include <3ds/types.h> -#include <3ds/svc.h> #include "sys_timer.h" #ifdef BIT #undef BIT diff --git a/tools/functions.make b/tools/functions.make index 1f07e0d381..4e784ff1b1 100644 --- a/tools/functions.make +++ b/tools/functions.make @@ -40,7 +40,7 @@ objcopy_plugin = $(OC) $(if $(filter yes, $(PLUGIN_USE_ELF)), -S -x, -O binary) else ifneq (,$(findstring sdl-sim,$(APP_TYPE))) objcopy = cp $(1) $(1).tmp;mv -f $(1).tmp $(2) # objcopy simulator objcopy_plugin = $(objcopy) -else ifneq (,$(findstring ctru,$(MODELNAME))) # 3dsxtool requires symbols +else ifneq (,$(findstring 3ds,$(MODELNAME))) # 3dsxtool requires symbols objcopy = cp $(1) $(1).tmp;mv -f $(1).tmp $(2) objcopy_plugin = $(objcopy) else From 8ad69d6649758fd76d1334d82362ea3c0d060548 Mon Sep 17 00:00:00 2001 From: Marc Andersen Date: Tue, 1 Sep 2026 21:32:17 +0200 Subject: [PATCH 142/408] debug_menu: exclude HIBY RAM info from simulators The "View RAM info" debug screen reports the host system's memory. It calls sysinfo(2) and reads /proc/meminfo, but it is guarded only by HIBY_LINUX. That breaks the win32 simulator for all seven HIBY_LINUX targets - agptekrocker, aigoerosq, hibyr1, hibyr3proii, hidizsap80max, xduoox20 and xduoox3ii - because HIBY_LINUX is still defined when the simulator is cross-compiled for Windows, and mingw has no : apps/debug_menu.c:146:10: fatal error: sys/sysinfo.h: No such file or directory Exclude simulators at all three sites - the include, the function and the menu entry - matching the !defined(SIMULATOR) guards already used elsewhere in this file. On a simulator the screen reports the developer machine's memory rather than the simulated device's, so it does not belong there on any host. Verified by building agptekrocker three ways: as a Windows simulator, which now succeeds; as a Linux simulator, which still succeeds; and for the device itself with arm-rockbox-linux-gnueabi, which still succeeds and keeps the screen. Provenance, per the AI disclosure requirement in docs/CONTRIBUTING: this change was drafted with Claude Code (Anthropic Claude Opus 5) at my direction. Patch set 3 adopts Solomon Peachy's review suggestion to key the guard off SIMULATOR rather than __linux__. Change-Id: If8a09da82d22118924a3375e05739dcd5640e11d --- apps/debug_menu.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/debug_menu.c b/apps/debug_menu.c index 51a1fcdc85..95e744f160 100644 --- a/apps/debug_menu.c +++ b/apps/debug_menu.c @@ -142,7 +142,7 @@ #include "iap.h" #endif -#ifdef HIBY_LINUX +#if defined(HIBY_LINUX) && !defined(SIMULATOR) #include #endif @@ -2808,7 +2808,7 @@ static bool dbg_bootflash_dump(void) { } #endif -#ifdef HIBY_LINUX +#if defined(HIBY_LINUX) && !defined(SIMULATOR) static bool view_ram_info(void) { struct simplelist_info info; @@ -2896,7 +2896,7 @@ static const struct { #ifdef __linux__ { "View CPU stats", dbg_cpuinfo }, #endif -#ifdef HIBY_LINUX +#if defined(HIBY_LINUX) && !defined(SIMULATOR) { "View RAM info", view_ram_info }, #endif #if (CONFIG_BATTERY_MEASURE != 0) && !defined(SIMULATOR) From 8eefac363815c2ca5fe6236f7733cef722f18f11 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Mon, 24 Aug 2026 15:42:03 +0200 Subject: [PATCH 143/408] quickscreen: prevent out-of-bounds viewports Fixes possible crash in case of pathological UI vp dimensions, when attempting to clear a QS item's vp. E.g. https://github.com/federicoplg/musicOS (commit c5e8570) configures a 1x1 UI viewport at x=319 y=239, resulting in left/right item viewports that have negative width or height. Minor additional simplifications with no effect on behavior: - Combine actions requiring cleanup in a setup function. Put it below the existing cleanup function - Rename quick_screen_quick to quickscreen_show, and quickscreen_run to quickscreen_main - Iterate over items using FOR_QS_ITEMS macro - Remove quickscreen_draw_item's int_value - quickscreen_update: immediately continue if qs skinned - Remove some obsolete #includes Change-Id: I49aea3be837b861bdcd5132e84fef858df72327b --- apps/gui/quickscreen.c | 187 +++++++++++++++++++++-------------------- apps/gui/quickscreen.h | 2 +- apps/gui/wps.c | 2 +- apps/menu.c | 2 +- apps/playlist_viewer.c | 2 +- apps/tree.c | 2 +- 6 files changed, 103 insertions(+), 94 deletions(-) diff --git a/apps/gui/quickscreen.c b/apps/gui/quickscreen.c index 087a879f9c..2c82c6b6fe 100644 --- a/apps/gui/quickscreen.c +++ b/apps/gui/quickscreen.c @@ -18,37 +18,32 @@ * ****************************************************************************/ -#include #include "config.h" -#include "system.h" #include "icons.h" #include "font.h" -#include "kernel.h" #include "misc.h" -#include "sound.h" #include "action.h" #include "settings_list.h" #include "lang.h" -#include "playlist.h" #include "viewport.h" -#include "audio.h" #include "quickscreen.h" #include "talk.h" -#include "list.h" #include "option_select.h" -#include "debug.h" -#include "shortcuts.h" #include "appevents.h" #include "statusbar-skinned.h" - /* 2 lines for each of the three vertical sections (top/middle/bottom). - If less space is available, the top and bottom each lose a line. */ + /* 2 lines each for the top, middle, and bottom section = 6 in total. + With less space, top and bottom sections lose a line = 4 in total. */ #define MIN_LINES (3*2) + #define MAX_NEEDED_LINES 10 - /* pixels between the 2 center items minimum or between text and icons, - * and between text and parent boundaries */ + + /* Minimum number of pixels between the 2 center items, between + text and icons, or between text and parent boundaries. */ #define MARGIN 10 -#define CENTER_ICONAREA_SIZE (MARGIN+8*2) +#define CENTER_ICONAREA_SIZE (MARGIN + 8*2) + +#define FOR_QS_ITEMS(i) for (int i = 0; i < QUICKSCREEN_ITEM_COUNT; i++) struct quickscreen { @@ -73,39 +68,34 @@ void quickscreen_set_skinned(enum screen_type screen, bool skinned) static void quickscreen_fix_viewports(struct quickscreen *qs, enum screen_type screen) { - int char_height, width, pad = 0; - int left_width = 0, right_width = 0, vert_lines; - unsigned char *s; + int line_height, width, pad = 0; + int left_width = 0, right_width = 0; + unsigned char *str; struct screen *display = &screens[screen]; struct viewport *parent = &qs->parent[screen]; struct viewport *vps = qs->vps[screen]; struct viewport *vp_icons = &qs->vp_icons[screen]; + + /* How many un-cropped lines can be displayed in the UI vp */ int nb_lines = viewport_get_nb_lines(parent); - - /* nb_lines only returns the number of fully visible lines, small screens - or really large fonts could cause problems with the calculation below. - */ if (nb_lines == 0) - nb_lines++; + nb_lines = 1; /* cropped single line */ - char_height = parent->height/nb_lines; + line_height = parent->height/nb_lines; /* center the icons VP first */ *vp_icons = *parent; - vp_icons->width = CENTER_ICONAREA_SIZE; /* abosulte smallest allowed */ + vp_icons->width = CENTER_ICONAREA_SIZE; /* absolute smallest allowed */ vp_icons->x = parent->x; - vp_icons->x += (parent->width-CENTER_ICONAREA_SIZE)/2; + vp_icons->x += (parent->width - CENTER_ICONAREA_SIZE)/2; vps[QUICKSCREEN_BOTTOM] = *parent; vps[QUICKSCREEN_TOP] = *parent; - /* depending on the space the top/buttom items use 1 or 2 lines */ - if (nb_lines < MIN_LINES) - vert_lines = 1; - else - vert_lines = 2; vps[QUICKSCREEN_TOP].y = parent->y; + + /* Top and bottom use 2 lines each, unless there's insufficient space */ vps[QUICKSCREEN_TOP].height = vps[QUICKSCREEN_BOTTOM].height - = vert_lines*char_height; + = (nb_lines < MIN_LINES ? 1 : 2)*line_height; vps[QUICKSCREEN_BOTTOM].y = parent->y + parent->height - vps[QUICKSCREEN_BOTTOM].height; @@ -116,29 +106,38 @@ static void quickscreen_fix_viewports(struct quickscreen *qs, enum screen_type s vps[QUICKSCREEN_BOTTOM].y -= MARGIN; } - vp_icons->y = vps[QUICKSCREEN_TOP].y - + vps[QUICKSCREEN_TOP].height; + vp_icons->y = vps[QUICKSCREEN_TOP].y + vps[QUICKSCREEN_TOP].height; vp_icons->height = vps[QUICKSCREEN_BOTTOM].y - vp_icons->y; /* adjust the left/right items widths to fit the screen nicely */ if (qs->items[QUICKSCREEN_LEFT]) { - s = P2STR(ID2P(qs->items[QUICKSCREEN_LEFT]->lang_id)); - left_width = display->getstringsize(s, NULL, NULL); + str = P2STR(ID2P(qs->items[QUICKSCREEN_LEFT]->lang_id)); + left_width = display->getstringsize(str, NULL, NULL); } if (qs->items[QUICKSCREEN_RIGHT]) { - s = P2STR(ID2P(qs->items[QUICKSCREEN_RIGHT]->lang_id)); - right_width = display->getstringsize(s, NULL, NULL); + str = P2STR(ID2P(qs->items[QUICKSCREEN_RIGHT]->lang_id)); + right_width = display->getstringsize(str, NULL, NULL); } width = MAX(left_width, right_width); + + /* crop text vp, if necessary */ if (width*2 + vp_icons->width > parent->width) - { /* crop text viewports */ - width = (parent->width - vp_icons->width)/2; + { + width = parent->width; + if (width > vp_icons->width) /* check if icons fit */ + width -= vp_icons->width; + else if (width > MARGIN) /* margin without icons */ + width -= MARGIN; + + if (width >= 2) + width /= 2; } + /* space for lager gap between between icons */ else - { /* add more gap in icons vp */ + { int excess = parent->width - vp_icons->width - width*2; if (excess > MARGIN*4) { @@ -157,11 +156,13 @@ static void quickscreen_fix_viewports(struct quickscreen *qs, enum screen_type s vps[QUICKSCREEN_RIGHT].x = parent->x + parent->width - width - pad; vps[QUICKSCREEN_RIGHT].width = width; - vps[QUICKSCREEN_LEFT].height = vps[QUICKSCREEN_RIGHT].height - = 2*char_height; - - vps[QUICKSCREEN_LEFT].y = vps[QUICKSCREEN_RIGHT].y - = parent->y + (parent->height/2) - char_height; + if (nb_lines >= 2) /* otherwise, use parent height and y position */ + { + vps[QUICKSCREEN_LEFT].height = vps[QUICKSCREEN_RIGHT].height + = 2*line_height; + vps[QUICKSCREEN_LEFT].y = vps[QUICKSCREEN_RIGHT].y + = parent->y + (parent->height/2) - line_height; + } /* shrink the icons vp by a few pixels if there is room so the arrows aren't drawn right next to the text */ @@ -177,10 +178,10 @@ static void quickscreen_fix_viewports(struct quickscreen *qs, enum screen_type s } /* text alignment */ - vps[QUICKSCREEN_LEFT].flags &= ~VP_FLAG_ALIGNMENT_MASK; /* left-aligned */ - vps[QUICKSCREEN_TOP].flags |= VP_FLAG_ALIGN_CENTER; /* centered */ - vps[QUICKSCREEN_BOTTOM].flags |= VP_FLAG_ALIGN_CENTER; /* centered */ - vps[QUICKSCREEN_RIGHT].flags &= ~VP_FLAG_ALIGNMENT_MASK;/* right aligned*/ + vps[QUICKSCREEN_LEFT].flags &= ~VP_FLAG_ALIGNMENT_MASK; /* left-aligned */ + vps[QUICKSCREEN_TOP].flags |= VP_FLAG_ALIGN_CENTER; /* centered */ + vps[QUICKSCREEN_BOTTOM].flags |= VP_FLAG_ALIGN_CENTER; /* centered */ + vps[QUICKSCREEN_RIGHT].flags &= ~VP_FLAG_ALIGNMENT_MASK; /* right-aligned */ vps[QUICKSCREEN_RIGHT].flags |= VP_FLAG_ALIGN_RIGHT; } @@ -189,13 +190,9 @@ static void quickscreen_draw_item(struct quickscreen *qs, struct screen *display enum quickscreen_item i, bool single_line) { char buf[MAX_PATH]; - int int_value; - unsigned const char *title, *value; - - title = P2STR(ID2P(qs->items[i]->lang_id)); - int_value = option_value_as_int(qs->items[i]); - value = option_get_valuestring(qs->items[i], buf, sizeof buf, int_value); - + const char *title = P2STR(ID2P(qs->items[i]->lang_id)); + const char *value = option_get_valuestring(qs->items[i], buf, sizeof buf, + option_value_as_int(qs->items[i])); if (single_line) { char text[MAX_PATH]; @@ -214,13 +211,13 @@ static void quickscreen_update(struct quickscreen *qs, enum quickscreen_item sel { FOR_NB_SCREENS(screen) { - struct screen *display = &screens[screen]; - struct viewport *vps = qs->vps[screen]; - if (qs_skinned[screen]) continue; - for (int i = 0; i < QUICKSCREEN_ITEM_COUNT; i++) + struct screen *display = &screens[screen]; + struct viewport *vps = qs->vps[screen]; + + FOR_QS_ITEMS(i) if (qs->items[i] == qs->items[selected]) { struct viewport *last_vp = display->set_viewport(&vps[i]); @@ -243,8 +240,7 @@ static void quickscreen_draw(struct quickscreen *qs, enum screen_type screen) struct viewport *last_vp = display->set_viewport(parent); display->clear_viewport(); - /* items */ - for (int i = 0; i < QUICKSCREEN_ITEM_COUNT; i++) + FOR_QS_ITEMS(i) if (qs->items[i]) { display->set_viewport(&vps[i]); @@ -252,22 +248,25 @@ static void quickscreen_draw(struct quickscreen *qs, enum screen_type screen) } /* icons */ - display->set_viewport(vp_icons); - if (qs->items[QUICKSCREEN_TOP]) - display->mono_bitmap(bitmap_icons_7x8[Icon_UpArrow], - (vp_icons->width/2) - 4, 0, 7, 8); + if (parent->width > CENTER_ICONAREA_SIZE && vp_icons->height >= 8) + { + display->set_viewport(vp_icons); + if (qs->items[QUICKSCREEN_TOP]) + display->mono_bitmap(bitmap_icons_7x8[Icon_UpArrow], + (vp_icons->width/2) - 4, 0, 7, 8); - if (qs->items[QUICKSCREEN_RIGHT]) - display->mono_bitmap(bitmap_icons_7x8[Icon_FastForward], - vp_icons->width - 8, (vp_icons->height/2) - 4, 7, 8); + if (qs->items[QUICKSCREEN_RIGHT]) + display->mono_bitmap(bitmap_icons_7x8[Icon_FastForward], + vp_icons->width - 8, (vp_icons->height/2) - 4, 7, 8); - if (qs->items[QUICKSCREEN_LEFT]) - display->mono_bitmap(bitmap_icons_7x8[Icon_FastBackward], - 0, (vp_icons->height/2) - 4, 7, 8); + if (qs->items[QUICKSCREEN_LEFT]) + display->mono_bitmap(bitmap_icons_7x8[Icon_FastBackward], + 0, (vp_icons->height/2) - 4, 7, 8); - if (qs->items[QUICKSCREEN_BOTTOM]) - display->mono_bitmap(bitmap_icons_7x8[Icon_DownArrow], - (vp_icons->width/2) - 4, vp_icons->height - 8, 7, 8); + if (qs->items[QUICKSCREEN_BOTTOM]) + display->mono_bitmap(bitmap_icons_7x8[Icon_DownArrow], + (vp_icons->width/2) - 4, vp_icons->height - 8, 7, 8); + } skin_mark_dirty(screen); display->set_viewport(last_vp); @@ -379,6 +378,7 @@ static int quickscreen_touchscreen_button(void) } #endif +/* Undo activity, viewport, and event listener setup. */ static void cleanup(void *parameter) { struct quickscreen *qs = (struct quickscreen *) parameter; @@ -387,7 +387,7 @@ static void cleanup(void *parameter) FOR_NB_SCREENS(i) { if (!qs_skinned[i]) - for (int j = 0; j < QUICKSCREEN_ITEM_COUNT; j++) + FOR_QS_ITEMS(j) screens[i].scroll_stop_viewport(&qs->vps[i][j]); viewportmanager_theme_undo(i, !(qs->result & QUICKSCREEN_GOTO_SHORTCUTS_MENU)); } @@ -398,16 +398,9 @@ static void cleanup(void *parameter) pop_current_activity(); } -static void quickscreen_run(struct quickscreen * qs) +/* Set up activity, viewport, and event listener. Draw initial Quickscreen. */ +static inline void setup(struct quickscreen *qs) { - int button; - enum quickscreen_item item; - /* To quit we need either : - * - a second press on the button that made us enter - * - an action taken while pressing the enter button, - * then release the enter button*/ - bool can_quit = false; - push_current_activity(ACTIVITY_QUICKSCREEN); FOR_NB_SCREENS(i) @@ -421,6 +414,21 @@ static void quickscreen_run(struct quickscreen * qs) quickscreen_draw(qs, i); } } + add_event_ex(GUI_EVENT_NEED_UI_UPDATE, false, quickscreen_draw_cb, qs); +} + +static void quickscreen_main(struct quickscreen * qs) +{ + /* To quit we need either : + * - a second press on the button that made us enter + * - an action taken while pressing the enter button, + * then release the enter button */ + bool can_quit = false; + int button; + enum quickscreen_item item; + + setup(qs); + /* Announce current selection on entering this screen. This is all queued up, but can be interrupted as soon as a setting is changed. */ @@ -435,7 +443,6 @@ static void quickscreen_run(struct quickscreen * qs) #ifdef HAVE_TOUCHSCREEN action_gesture_reset(); #endif - add_event_ex(GUI_EVENT_NEED_UI_UPDATE, false, quickscreen_draw_cb, qs); while (true) { button = get_action(CONTEXT_QUICKSCREEN, HZ/5); @@ -480,17 +487,19 @@ static void quickscreen_run(struct quickscreen * qs) } /* Notify that we're exiting this screen */ cond_talk_ids_fq(VOICE_OK); + cleanup(qs); } -int quick_screen_quick(int button_enter) +/* Entry point for external callers */ +int quickscreen_show(int button_enter) { struct quickscreen qs; qs.button_enter = button_enter; qs.result = QUICKSCREEN_OK; qs.volume_item = QUICKSCREEN_ITEM_COUNT; - for (int i = 0; i < 4; ++i) + FOR_QS_ITEMS(i) { qs.items[i] = global_settings.qs_items[i]; @@ -501,7 +510,7 @@ int quick_screen_quick(int button_enter) qs.volume_item = i; } - quickscreen_run(&qs); + quickscreen_main(&qs); if (qs.result & QUICKSCREEN_CHANGED) settings_save(); diff --git a/apps/gui/quickscreen.h b/apps/gui/quickscreen.h index 669da7a277..f59f0d3058 100644 --- a/apps/gui/quickscreen.h +++ b/apps/gui/quickscreen.h @@ -44,7 +44,7 @@ enum quickscreen_return { QUICKSCREEN_CHANGED = 0x4, }; -extern int quick_screen_quick(int button_enter); +int quickscreen_show(int button_enter); void quickscreen_set_skinned(enum screen_type screen, bool skinned); bool is_setting_quickscreenable(const struct settings_list *setting); diff --git a/apps/gui/wps.c b/apps/gui/wps.c index dec58008b8..3a9ae8a6f4 100644 --- a/apps/gui/wps.c +++ b/apps/gui/wps.c @@ -982,7 +982,7 @@ long gui_wps_show(void) bool enter_shortcuts_menu = global_settings.shortcuts_replaces_qs; if (!enter_shortcuts_menu) { - int ret = quick_screen_quick(button); + int ret = quickscreen_show(button); if (ret == QUICKSCREEN_IN_USB) return GO_TO_ROOT; else if (ret == QUICKSCREEN_GOTO_SHORTCUTS_MENU) diff --git a/apps/menu.c b/apps/menu.c index b4c4a3d419..f81c1665a5 100644 --- a/apps/menu.c +++ b/apps/menu.c @@ -472,7 +472,7 @@ int do_menu(const struct menu_item_ex *start_menu, int *start_selected, else if (action == ACTION_STD_QUICKSCREEN) { if (global_settings.shortcuts_replaces_qs || - quick_screen_quick(action) == QUICKSCREEN_GOTO_SHORTCUTS_MENU) + quickscreen_show(action) == QUICKSCREEN_GOTO_SHORTCUTS_MENU) { int last_screen = global_status.last_screen; global_status.last_screen = GO_TO_SHORTCUTMENU; diff --git a/apps/playlist_viewer.c b/apps/playlist_viewer.c index 639007ec10..827ceb4b1b 100644 --- a/apps/playlist_viewer.c +++ b/apps/playlist_viewer.c @@ -1153,7 +1153,7 @@ enum playlist_viewer_result playlist_viewer_ex(const char* filename, case ACTION_STD_QUICKSCREEN: if (!global_settings.shortcuts_replaces_qs) { - if (quick_screen_quick(button) == + if (quickscreen_show(button) == QUICKSCREEN_GOTO_SHORTCUTS_MENU) /* currently disabled */ { /* QuickScreen defers skin updates when popping its activity diff --git a/apps/tree.c b/apps/tree.c index 85cf14e465..e6e5833e31 100644 --- a/apps/tree.c +++ b/apps/tree.c @@ -876,7 +876,7 @@ static int dirbrowse(void) break; else if (!enter_shortcuts_menu) { - int ret = quick_screen_quick(button); + int ret = quickscreen_show(button); if (ret == QUICKSCREEN_IN_USB) reload_dir = true; else if (ret == QUICKSCREEN_GOTO_SHORTCUTS_MENU) From 81cf12098308b7961c19b56aa96ff22ff7226d25 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Mon, 24 Aug 2026 21:25:50 +0200 Subject: [PATCH 144/408] quickscreen: always refresh SBS when leaving Eliminate special case for transition to the Shortcuts menu. The SBS refresh doesn't cause a screen update anymore, and has negligible cost, so do it regardless. Reverts the changes to quickscreen.c introduced in commit dfd9c10 ("Eliminate skin updates in between activities") + simplify quickscreen_draw_item (no effect on behavior) Change-Id: I3fca976de554c720cb69a41566fd0d0a7e6ea92d --- apps/gui/quickscreen.c | 26 ++++++++++++-------------- apps/playlist_viewer.c | 12 ++++-------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/apps/gui/quickscreen.c b/apps/gui/quickscreen.c index 2c82c6b6fe..90bbde6db6 100644 --- a/apps/gui/quickscreen.c +++ b/apps/gui/quickscreen.c @@ -185,14 +185,14 @@ static void quickscreen_fix_viewports(struct quickscreen *qs, enum screen_type s vps[QUICKSCREEN_RIGHT].flags |= VP_FLAG_ALIGN_RIGHT; } -/* Draw QS item into current viewport */ -static void quickscreen_draw_item(struct quickscreen *qs, struct screen *display, - enum quickscreen_item i, bool single_line) +/* Draw settings item into current viewport */ +static void quickscreen_draw_setting(const struct settings_list *item, + struct screen *display, bool single_line) { char buf[MAX_PATH]; - const char *title = P2STR(ID2P(qs->items[i]->lang_id)); - const char *value = option_get_valuestring(qs->items[i], buf, sizeof buf, - option_value_as_int(qs->items[i])); + const char *title = P2STR(ID2P(item->lang_id)); + const char *value = option_get_valuestring(item, buf, sizeof buf, + option_value_as_int(item)); if (single_line) { char text[MAX_PATH]; @@ -222,7 +222,8 @@ static void quickscreen_update(struct quickscreen *qs, enum quickscreen_item sel { struct viewport *last_vp = display->set_viewport(&vps[i]); display->clear_viewport(); - quickscreen_draw_item(qs, display, i, viewport_get_nb_lines(&vps[i]) < 2); + quickscreen_draw_setting(qs->items[i], display, + viewport_get_nb_lines(&vps[i]) < 2); display->set_viewport(last_vp); } @@ -244,7 +245,8 @@ static void quickscreen_draw(struct quickscreen *qs, enum screen_type screen) if (qs->items[i]) { display->set_viewport(&vps[i]); - quickscreen_draw_item(qs, display, i, viewport_get_nb_lines(&vps[i]) < 2); + quickscreen_draw_setting(qs->items[i], display, + viewport_get_nb_lines(&vps[i]) < 2); } /* icons */ @@ -389,13 +391,9 @@ static void cleanup(void *parameter) if (!qs_skinned[i]) FOR_QS_ITEMS(j) screens[i].scroll_stop_viewport(&qs->vps[i][j]); - viewportmanager_theme_undo(i, !(qs->result & QUICKSCREEN_GOTO_SHORTCUTS_MENU)); + viewportmanager_theme_undo(i, true); } - /* Eliminate flashing of parent during transition to Shortcuts */ - if (qs->result & QUICKSCREEN_GOTO_SHORTCUTS_MENU) - pop_current_activity_without_refresh(); - else - pop_current_activity(); + pop_current_activity(); } /* Set up activity, viewport, and event listener. Draw initial Quickscreen. */ diff --git a/apps/playlist_viewer.c b/apps/playlist_viewer.c index 827ceb4b1b..e35700732c 100644 --- a/apps/playlist_viewer.c +++ b/apps/playlist_viewer.c @@ -1151,16 +1151,12 @@ enum playlist_viewer_result playlist_viewer_ex(const char* filename, goto exit; #ifdef HAVE_QUICKSCREEN case ACTION_STD_QUICKSCREEN: + /* Shortcuts menu currently disabled in Playlist Viewer */ if (!global_settings.shortcuts_replaces_qs) { - if (quickscreen_show(button) == - QUICKSCREEN_GOTO_SHORTCUTS_MENU) /* currently disabled */ - { - /* QuickScreen defers skin updates when popping its activity - to switch to Shortcuts Menu, so make up for that here: */ - FOR_NB_SCREENS(i) - skin_update(CUSTOM_STATUSBAR, i, SKIN_REFRESH_ALL); - } + /* QUICKSCREEN_GOTO_SHORTCUTS_MENU + return value ignored for now */ + quickscreen_show(button); update_playlist(true); update_gui(&playlist_lists, true); } From 1a37ac8568f98e10f7e325deda7c0f03506fdd0b Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Thu, 3 Sep 2026 01:27:57 +0200 Subject: [PATCH 145/408] tree: skip redundant SBS refresh after Quickscreen missing from 81cf120983 Change-Id: I161a777e62d0c8563cd7b588678549e2385c6fc9 --- apps/tree.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/tree.c b/apps/tree.c index e6e5833e31..ba8aa96c2b 100644 --- a/apps/tree.c +++ b/apps/tree.c @@ -871,6 +871,7 @@ static int dirbrowse(void) #ifdef HAVE_QUICKSCREEN case ACTION_STD_QUICKSCREEN: { + /* Shortcuts Menu currently disabled in browse filter modes */ bool enter_shortcuts_menu = global_settings.shortcuts_replaces_qs; if (enter_shortcuts_menu && *tc.dirfilter >= NUM_FILTER_MODES) break; @@ -893,13 +894,6 @@ static int dirbrowse(void) else return exit_to_new_screen(shortcut_ret); } - else if (enter_shortcuts_menu) /* currently disabled */ - { - /* QuickScreen defers skin updates, popping its activity, when - switching to Shortcuts Menu, so make up for that here: */ - FOR_NB_SCREENS(i) - skin_update(CUSTOM_STATUSBAR, i, SKIN_REFRESH_ALL); - } restore = do_restore_display; break; From 284af2aee8f64de805fa2f7e0e664afc01305067 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Thu, 3 Sep 2026 16:47:45 +0100 Subject: [PATCH 146/408] lastfm_scrobbler: don't include rbversion.h I intended to remove this in commit a8f8aa40b9 ("lastfm_scrobbler: fetch rbversion from plugin API") but apparently forgot to do so, so the scrobbler plugin was still rebuilding itself when RBVERSION changes. Removing the header fixes that. Change-Id: I6740d72ad35f5037a6a4a7580559a6c980b3225b --- apps/plugins/lastfm_scrobbler.c | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/plugins/lastfm_scrobbler.c b/apps/plugins/lastfm_scrobbler.c index e3be350915..f175b07fab 100644 --- a/apps/plugins/lastfm_scrobbler.c +++ b/apps/plugins/lastfm_scrobbler.c @@ -66,7 +66,6 @@ Example All fields except those marked (optional) above are required. */ -#include "rbversion.h" #include "plugin.h" #include "lib/configfile.h" From 569c2a53c831d06a002a7a2a56ef986bc2a69774 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Fri, 4 Sep 2026 17:32:48 +0200 Subject: [PATCH 147/408] quickscreen: fix string size calculation in quickscreen_fix_viewports The parent vp wasn't set before checking the string size, so, in certain scenarios, the size was calculated using a different font than the one configured for the UI viewport. Change-Id: I66bb2e496598811f6169a118de5cb83b451fa292 --- apps/gui/quickscreen.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/gui/quickscreen.c b/apps/gui/quickscreen.c index 90bbde6db6..6b55bdbf1e 100644 --- a/apps/gui/quickscreen.c +++ b/apps/gui/quickscreen.c @@ -71,7 +71,6 @@ static void quickscreen_fix_viewports(struct quickscreen *qs, enum screen_type s int line_height, width, pad = 0; int left_width = 0, right_width = 0; unsigned char *str; - struct screen *display = &screens[screen]; struct viewport *parent = &qs->parent[screen]; struct viewport *vps = qs->vps[screen]; struct viewport *vp_icons = &qs->vp_icons[screen]; @@ -113,12 +112,12 @@ static void quickscreen_fix_viewports(struct quickscreen *qs, enum screen_type s if (qs->items[QUICKSCREEN_LEFT]) { str = P2STR(ID2P(qs->items[QUICKSCREEN_LEFT]->lang_id)); - left_width = display->getstringsize(str, NULL, NULL); + left_width = font_getstringsize(str, NULL, NULL, parent->font); } if (qs->items[QUICKSCREEN_RIGHT]) { str = P2STR(ID2P(qs->items[QUICKSCREEN_RIGHT]->lang_id)); - right_width = display->getstringsize(str, NULL, NULL); + right_width = font_getstringsize(str, NULL, NULL, parent->font); } width = MAX(left_width, right_width); From 20fa5f017df15bc6354c9e7734c71e319de9eb1f Mon Sep 17 00:00:00 2001 From: Mauricio Garrido Date: Fri, 4 Sep 2026 08:23:04 -0600 Subject: [PATCH 148/408] 3ds: Another small fix. This commit makes removes some remaining 3ds headers and functions from common console headers. Change-Id: I347173bf1f3604ff29a85bd52a9dd6130b4ff4b2 --- firmware/target/hosted/game_console/3ds/pcm-3ds.c | 3 +++ .../target/hosted/game_console/3ds/system-3ds.c | 1 + .../target/hosted/game_console/lib/bfile/bfile.h | 4 +++- .../target/hosted/game_console/lib/sys_thread.h | 1 - .../target/hosted/game_console/system-console.h | 14 ++++---------- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/firmware/target/hosted/game_console/3ds/pcm-3ds.c b/firmware/target/hosted/game_console/3ds/pcm-3ds.c index 01335febde..3279429223 100644 --- a/firmware/target/hosted/game_console/3ds/pcm-3ds.c +++ b/firmware/target/hosted/game_console/3ds/pcm-3ds.c @@ -33,6 +33,9 @@ #include <3ds/synchronization.h> #include <3ds/allocator/linear.h> +#include "sys_atomic.h" +#include "sys_thread.h" + #include "config.h" #include "debug.h" #include "sound.h" diff --git a/firmware/target/hosted/game_console/3ds/system-3ds.c b/firmware/target/hosted/game_console/3ds/system-3ds.c index 473a91c699..b6dc917dce 100644 --- a/firmware/target/hosted/game_console/3ds/system-3ds.c +++ b/firmware/target/hosted/game_console/3ds/system-3ds.c @@ -25,6 +25,7 @@ /* this includes a couple of 3ds headers */ #include "bfile.h" +#include "sys_timer.h" #include <3ds/types.h> #include <3ds/allocator/linear.h> diff --git a/firmware/target/hosted/game_console/lib/bfile/bfile.h b/firmware/target/hosted/game_console/lib/bfile/bfile.h index bdaeb3810e..f6ec1f31fd 100644 --- a/firmware/target/hosted/game_console/lib/bfile/bfile.h +++ b/firmware/target/hosted/game_console/lib/bfile/bfile.h @@ -6,7 +6,7 @@ static const int defaultPageSize = 0x20000; // default page buffer size, 128 kB // NewPageReader creates a new PageReader struct with pageSize. -PageReader *NewPageReader(Handle file, s64 pageSize); +PageReader *NewPageReader(sys_handle_type file, s64 pageSize); // Free all memory associated to the PageReader pointer. void PageReader_Free(PageReader *p); @@ -17,10 +17,12 @@ int_error_t PageReader_ReadAt(PageReader *p, u8 *buffer, size_t size, off_t offs // WriteAt writes size bytes from the PageReader starting at offset. int_error_t PageReader_WriteAt(PageReader *p, u8 *buffer, size_t size, off_t offset, bool flush_cache); +#ifdef N3DS // NewPageReader creates a new PageReader struct with pageSize, for directory use. PageReader *NewPageReaderDirectory(Handle file, s64 pageSize); // ReadAt reads one directory entry from memory. int_error_t PageReader_ReadDir(PageReader *p, FS_DirectoryEntry *entry); +#endif #endif /* _B_FILE_H_ */ diff --git a/firmware/target/hosted/game_console/lib/sys_thread.h b/firmware/target/hosted/game_console/lib/sys_thread.h index cbdc294720..e42c32faa4 100644 --- a/firmware/target/hosted/game_console/lib/sys_thread.h +++ b/firmware/target/hosted/game_console/lib/sys_thread.h @@ -21,7 +21,6 @@ #ifndef __SYSTHREAD_H__ #define __SYSTHREAD_H__ -#include "sys_types.h" #include "sys_atomic.h" #include "thread.h" diff --git a/firmware/target/hosted/game_console/system-console.h b/firmware/target/hosted/game_console/system-console.h index fe11b74445..abe27cd4b8 100644 --- a/firmware/target/hosted/game_console/system-console.h +++ b/firmware/target/hosted/game_console/system-console.h @@ -17,16 +17,10 @@ * KIND, either express or implied. * ****************************************************************************/ -#ifndef _SYSTEM_SDL_H_ -#define _SYSTEM_SDL_H_ +#ifndef _SYSTEM_CONSOLE_H_ +#define _SYSTEM_CONSOLE_H_ #include -#include - -#include "sys_timer.h" -#ifdef BIT -#undef BIT -#endif #include "config.h" #include "gcc_extensions.h" @@ -57,7 +51,7 @@ void sim_kernel_shutdown(void); void sys_handle_argv(int argc, char *argv[]); void gui_message_loop(void); void sim_do_exit(void) NORETURN_ATTR; -void sdl_sys_quit(void); +void console_sys_quit(void); void mcuhwc_init(void); void mcuhwc_close(void); @@ -67,4 +61,4 @@ extern bool showremote; extern double display_zoom; extern long start_tick; -#endif /* _SYSTEM_SDL_H_ */ +#endif /* _SYSTEM_CONSOLE_H_ */ From 45bd2b970b3475bfef05eae00f16dd669ef9af30 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Sat, 5 Sep 2026 10:39:54 -0400 Subject: [PATCH 149/408] [Bugfix] FS#13975 - Keyremap plugin: text importer allow multiple ACTION_NONE entries importing discards duplicate entries based on context + action code however you may want multiple entries to map to the same action and you can't do it within the same context instead only consider an entry as a duplicate if they have the same context, actioncode, button, and prebutton Change-Id: I5aba3459505987ba37e26758aec62f02fcf8165a --- apps/plugins/keyremap.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/plugins/keyremap.c b/apps/plugins/keyremap.c index 9917bc567e..a224cc292a 100644 --- a/apps/plugins/keyremap.c +++ b/apps/plugins/keyremap.c @@ -746,11 +746,14 @@ static int keymap_add_button_entry(int context, int action_code, for (int i = 0; i < ctx_data.act_count; i++) { if (ctx_data.act_map[i].context == context && - ctx_data.act_map[i].map.action_code == action_code) + ctx_data.act_map[i].map.action_code == action_code && + ctx_data.act_map[i].map.button_code == button_code && + ctx_data.act_map[i].map.pre_button_code == pre_button_code) { - /*Duplicate -- Update the existing entry */ - ctx_data.act_map[i].map.button_code = button_code; - ctx_data.act_map[i].map.pre_button_code = pre_button_code; + /*Duplicate -- Use the existing entry */ + /* FS#13975 allow multiple entries to map same action code */ + logf("keyremap: skipping duplicate @ [%d] ctx: %d, act: %d btn: %d pbtn: %d", + i, context, action_code, button_code, pre_button_code); return ctx_data.act_count; } } From da207d606764a0716276a452bd137311912c791d Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Fri, 4 Sep 2026 17:37:14 +0200 Subject: [PATCH 150/408] quickscreen: refactor quickscreen_fix_viewports Reduces GNU complexity score from 7 to 2. Use separate inline functions that group setting - x-position and width for left and right text viewport - y-position and height for all text viewports - position/dimensions for icons viewport Change-Id: I14f4eee1f4d6fefa9acbf4a37970a535e77990b1 --- apps/gui/quickscreen.c | 179 ++++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 81 deletions(-) diff --git a/apps/gui/quickscreen.c b/apps/gui/quickscreen.c index 6b55bdbf1e..3cf4a58a75 100644 --- a/apps/gui/quickscreen.c +++ b/apps/gui/quickscreen.c @@ -66,115 +66,132 @@ void quickscreen_set_skinned(enum screen_type screen, bool skinned) qs_skinned[screen] = skinned; } -static void quickscreen_fix_viewports(struct quickscreen *qs, enum screen_type screen) +/* Set up icons viewport */ +static inline void setup_icons(struct viewport *vp_icons, int excess_width, + int top_y, int top_height, int bottom_y) { - int line_height, width, pad = 0; - int left_width = 0, right_width = 0; - unsigned char *str; - struct viewport *parent = &qs->parent[screen]; - struct viewport *vps = qs->vps[screen]; - struct viewport *vp_icons = &qs->vp_icons[screen]; + vp_icons->x += (vp_icons->width - CENTER_ICONAREA_SIZE - excess_width)/2; + vp_icons->y = top_y + top_height; - /* How many un-cropped lines can be displayed in the UI vp */ - int nb_lines = viewport_get_nb_lines(parent); - if (nb_lines == 0) - nb_lines = 1; /* cropped single line */ + vp_icons->width = CENTER_ICONAREA_SIZE + excess_width; + vp_icons->height = bottom_y - vp_icons->y; - line_height = parent->height/nb_lines; - - /* center the icons VP first */ - *vp_icons = *parent; - vp_icons->width = CENTER_ICONAREA_SIZE; /* absolute smallest allowed */ - vp_icons->x = parent->x; - vp_icons->x += (parent->width - CENTER_ICONAREA_SIZE)/2; - - vps[QUICKSCREEN_BOTTOM] = *parent; - vps[QUICKSCREEN_TOP] = *parent; - vps[QUICKSCREEN_TOP].y = parent->y; - - /* Top and bottom use 2 lines each, unless there's insufficient space */ - vps[QUICKSCREEN_TOP].height = vps[QUICKSCREEN_BOTTOM].height - = (nb_lines < MIN_LINES ? 1 : 2)*line_height; - vps[QUICKSCREEN_BOTTOM].y - = parent->y + parent->height - vps[QUICKSCREEN_BOTTOM].height; - - /* enough space vertically, so put a nice margin */ - if (nb_lines >= MAX_NEEDED_LINES) + /* Shrink icons vp by a few pixels if there is room, + so the arrows aren't drawn right next to the text */ + if (vp_icons->width > CENTER_ICONAREA_SIZE*2) { - vps[QUICKSCREEN_TOP].y += MARGIN; - vps[QUICKSCREEN_BOTTOM].y -= MARGIN; + vp_icons->x += CENTER_ICONAREA_SIZE*2/6; + vp_icons->width -= CENTER_ICONAREA_SIZE*2/3; } - - vp_icons->y = vps[QUICKSCREEN_TOP].y + vps[QUICKSCREEN_TOP].height; - vp_icons->height = vps[QUICKSCREEN_BOTTOM].y - vp_icons->y; - - /* adjust the left/right items widths to fit the screen nicely */ - if (qs->items[QUICKSCREEN_LEFT]) + if (vp_icons->height > CENTER_ICONAREA_SIZE*2) { - str = P2STR(ID2P(qs->items[QUICKSCREEN_LEFT]->lang_id)); - left_width = font_getstringsize(str, NULL, NULL, parent->font); + vp_icons->y += CENTER_ICONAREA_SIZE*2/6; + vp_icons->height -= CENTER_ICONAREA_SIZE*2/3; } - if (qs->items[QUICKSCREEN_RIGHT]) +} + +/* Set y-position and height for all text viewports */ +static inline void set_y_axis(struct viewport *left, struct viewport *right, + struct viewport *top, struct viewport *bottom) +{ + int parent_height = top->height; + /* Displayable lines in UI vp. 1 if no full line can be displayed */ + int nb_lines = viewport_get_nb_lines(top) ?: 1; + int line_height = parent_height/nb_lines; + + /* Top and bottom use 2 lines each, only if there's enough space */ + top->height = line_height; + if (nb_lines >= MIN_LINES) + top->height *= 2; + bottom->height = top->height; + bottom->y += parent_height - bottom->height; + + if (nb_lines >= MAX_NEEDED_LINES) /* more than enough vertical space */ { - str = P2STR(ID2P(qs->items[QUICKSCREEN_RIGHT]->lang_id)); - right_width = font_getstringsize(str, NULL, NULL, parent->font); + top->y += MARGIN; + bottom->y -= MARGIN; } - - width = MAX(left_width, right_width); - - /* crop text vp, if necessary */ - if (width*2 + vp_icons->width > parent->width) + if (nb_lines >= 2) /* Single line uses parent height and y position */ { - width = parent->width; - if (width > vp_icons->width) /* check if icons fit */ - width -= vp_icons->width; - else if (width > MARGIN) /* margin without icons */ + left->height = right->height = 2*line_height; + right->y += (parent_height/2) - line_height; + left->y = right->y; + } +} + +/* Set x-position and width for left and right text viewport. + Returns excess horizontal space, after accounting for icon area size. */ +static inline int set_x_axis(struct viewport *left, struct viewport *right, + int width) +{ + int pad = 0, excess = 0; + int parent_width = left->width; + + if (width*2 + CENTER_ICONAREA_SIZE > parent_width) + { + /* Shrink left and right text vps */ + width = parent_width; + if (width > CENTER_ICONAREA_SIZE) /* Check if icons fit */ + width -= CENTER_ICONAREA_SIZE; + else if (width > MARGIN) /* Check if margin without icons fits */ width -= MARGIN; if (width >= 2) width /= 2; } - /* space for lager gap between between icons */ else { - int excess = parent->width - vp_icons->width - width*2; + /* space for padding or for excess gap between icons */ + excess = parent_width - CENTER_ICONAREA_SIZE - width*2; if (excess > MARGIN*4) { pad = MARGIN; excess -= MARGIN*2; } - vp_icons->x -= excess/2; - vp_icons->width += excess; } + left->x += pad; + right->x += parent_width - width - pad; + right->width = left->width = width; - vps[QUICKSCREEN_LEFT] = *parent; - vps[QUICKSCREEN_LEFT].x = parent->x + pad; - vps[QUICKSCREEN_LEFT].width = width; + return excess; +} - vps[QUICKSCREEN_RIGHT] = *parent; - vps[QUICKSCREEN_RIGHT].x = parent->x + parent->width - width - pad; - vps[QUICKSCREEN_RIGHT].width = width; +/* Set up all QS viewports */ +static void quickscreen_setup_viewports(struct quickscreen *qs, + enum screen_type screen) +{ + struct viewport *parent = &qs->parent[screen]; + struct viewport *vps = qs->vps[screen]; + int width = 0, excess; + const char *str; - if (nb_lines >= 2) /* otherwise, use parent height and y position */ + qs->vp_icons[screen] = *parent; + FOR_QS_ITEMS(i) + vps[i] = *parent; + + /* Calculate left and right item's maximum title width */ + if (qs->items[QUICKSCREEN_LEFT]) { - vps[QUICKSCREEN_LEFT].height = vps[QUICKSCREEN_RIGHT].height - = 2*line_height; - vps[QUICKSCREEN_LEFT].y = vps[QUICKSCREEN_RIGHT].y - = parent->y + (parent->height/2) - line_height; + str = P2STR(ID2P(qs->items[QUICKSCREEN_LEFT]->lang_id)); + width = font_getstringsize(str, NULL, NULL, parent->font); + } + if (qs->items[QUICKSCREEN_RIGHT]) + { + str = P2STR(ID2P(qs->items[QUICKSCREEN_RIGHT]->lang_id)); + width = MAX(font_getstringsize(str, NULL, NULL, parent->font), width); } - /* shrink the icons vp by a few pixels if there is room so the arrows - aren't drawn right next to the text */ - if (vp_icons->width > CENTER_ICONAREA_SIZE*2) - { - vp_icons->width -= CENTER_ICONAREA_SIZE*2/3; - vp_icons->x += CENTER_ICONAREA_SIZE*2/6; - } - if (vp_icons->height > CENTER_ICONAREA_SIZE*2) - { - vp_icons->height -= CENTER_ICONAREA_SIZE*2/3; - vp_icons->y += CENTER_ICONAREA_SIZE*2/6; - } + /* Set x-position and width for left and right text viewports. Top and bottom + use entire width. Check for excess space after accounting for icon area */ + excess = set_x_axis(&vps[QUICKSCREEN_LEFT], &vps[QUICKSCREEN_RIGHT], width); + + /* Set y-position and height for all text viewports */ + set_y_axis(&vps[QUICKSCREEN_LEFT], &vps[QUICKSCREEN_RIGHT], + &vps[QUICKSCREEN_TOP], &vps[QUICKSCREEN_BOTTOM]); + + /* Configure icons viewport based on text vp dimensions and excess width */ + setup_icons(&qs->vp_icons[screen], excess, vps[QUICKSCREEN_TOP].y, + vps[QUICKSCREEN_TOP].height, vps[QUICKSCREEN_BOTTOM].y); /* text alignment */ vps[QUICKSCREEN_LEFT].flags &= ~VP_FLAG_ALIGNMENT_MASK; /* left-aligned */ @@ -407,7 +424,7 @@ static inline void setup(struct quickscreen *qs) viewportmanager_theme_enable(i, true, &qs->parent[i]); if (!qs_skinned[i]) { - quickscreen_fix_viewports(qs, i); + quickscreen_setup_viewports(qs, i); quickscreen_draw(qs, i); } } From 2ec4760117236f598b60e15224ef72bcbeb7c288 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Sat, 5 Sep 2026 17:02:05 +0200 Subject: [PATCH 151/408] disktidy: fix blank "Last Run" screen in some themes Listen to GUI_EVENT_NEED_UI_UPDATE events and redraw, so screen doesn't disappear if a theme draws over the UI vp. Occurs in situations when the SBS is redrawn after waking the screen, or when the song changes. Change-Id: I062e802959ab9d34d8c04b7f82da6b87efb5d739 --- apps/plugins/disktidy.c | 50 +++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/apps/plugins/disktidy.c b/apps/plugins/disktidy.c index 993a68a2db..c65be33d43 100644 --- a/apps/plugins/disktidy.c +++ b/apps/plugins/disktidy.c @@ -51,6 +51,12 @@ struct run_statistics { #endif }; +struct stats_text { + struct viewport vp; + char** arr; + int len; +}; + struct tidy_type { char filestring[64]; int pre; @@ -223,9 +229,23 @@ static bool load_run_stats(void) return load_success; } +static void display_run_stats_cb(unsigned short id, void *data, void *userdata) +{ + (void)id; + (void)data; + struct stats_text *text = (struct stats_text *) userdata; + display_text(text->len, text->arr, NULL, &text->vp, false); +} + +static void display_run_stats_cleanup(void *userdata) +{ + struct stats_text *text = (struct stats_text *) userdata; + rb->remove_event_ex(GUI_EVENT_NEED_UI_UPDATE, display_run_stats_cb, text); +} + static enum plugin_status display_run_stats(void) { - struct viewport vp; + struct stats_text text; if (!load_run_stats()) { rb->splash(HZ * 2, "Unable to load last run stats"); @@ -307,22 +327,22 @@ static enum plugin_status display_run_stats(void) dirs_removed , run_stats.dirs_removed == 1 ? "dir" : "dirs", "", run_time , "", }; - char** text_arr = last_run_text; - int len = ARRAYLEN(last_run_text); + text.arr = last_run_text; + text.len = ARRAYLEN(last_run_text); if (!num_removed) { /* Hide superfluous zeros if nothing was removed */ - text_arr[len - 9] = ""; - text_arr[len - 8] = run_time; - text_arr[len - 7] = ""; - len -= 6; + text.arr[text.len - 9] = ""; + text.arr[text.len - 8] = run_time; + text.arr[text.len - 7] = ""; + text.len -= 6; } #if CONFIG_RTC sbs_has_title = rb->sb_set_title_text(last_run, Icon_NOICON, SCREEN_MAIN); if (sbs_has_title) { - text_arr += 2; - len -=2; + text.arr += 2; + text.len -=2; #else sbs_has_title = rb->sb_set_title_text("Last Run", Icon_NOICON, SCREEN_MAIN); if (sbs_has_title) @@ -331,21 +351,23 @@ static enum plugin_status display_run_stats(void) rb->send_event(GUI_EVENT_ACTIONREDRAW, (void*)1); } - rb->viewport_set_defaults(&vp, SCREEN_MAIN); + rb->viewport_set_defaults(&text.vp, SCREEN_MAIN); - if (display_text(len, text_arr, NULL, &vp, false)) - { + if (display_text(text.len, text.arr, NULL, &text.vp, false)) return PLUGIN_USB_CONNECTED; - } + + rb->add_event_ex(GUI_EVENT_NEED_UI_UPDATE, false, display_run_stats_cb, &text); while (true) /* keep info on screen until cancelled */ { int button = rb->get_action(CONTEXT_STD, HZ/2); if (button == ACTION_STD_CANCEL || button == ACTION_STD_MENU) break; - if (rb->default_event_handler(button) == SYS_USB_CONNECTED) + if (rb->default_event_handler_ex(button, display_run_stats_cleanup, &text) + == SYS_USB_CONNECTED) return PLUGIN_USB_CONNECTED; } + display_run_stats_cleanup(&text); return PLUGIN_OK; } From d2ae775f0a7ff94b1f925af00a0ad219c71e901c Mon Sep 17 00:00:00 2001 From: David Cormier Date: Thu, 27 Aug 2026 23:22:37 -0300 Subject: [PATCH 152/408] ipod6g: add composite video output driver Add NTSC composite output for the iPod Classic 6G/7G using the S5L8702 video processor, mixer, and encoder. Reconstruct the setup used by the original firmware and mirror the 320x240 LCD in a centered 648x432 viewport. Expose the driver through HAVE_COMPOSITE_VIDEO_OUT and a target-neutral videoout interface. Keep the S5L8702 MMIO layout and register definitions with the other SoC definitions in s5l87xx.h. Convert LCD updates from RGB565 to planar YUV420. MPEG playback copies decoded YUV420 planes directly, avoiding an RGB round trip. Hold output clocks and CPU boost only while the memory-backed layer is active, and restore them on disable or power-off. Defer dock identification out of the serial tick because the accessory resistor ADC path sleeps. Auto detection recognizes the measured Philips DCP750/37 resistor range; manual mode can qualify other attached docks. Leave interrupts enabled during the encoder's 10 ms reset wait so PCM DMA can service linked-buffer completions when a dock is inserted during audio playback. Keep reset assertion, reset release, all SVID register writes, and pipeline start atomic so the composite setup cannot interleave. Hardware tested on an iPod Classic 6G/7G with a Philips DCP750/37 for correct colors and geometry, stable UI mirroring, and full-screen MPEG playback. Change-Id: I669f2477d5cc707b48f7d24384c713d874a80e3f --- firmware/SOURCES | 3 + firmware/export/config/ipod6g.h | 5 + firmware/export/s5l87xx.h | 73 ++ firmware/export/videoout.h | 50 + firmware/target/arm/s5l8702/ipod6g/power-6g.c | 7 + .../target/arm/s5l8702/ipod6g/serial-6g.c | 61 ++ .../target/arm/s5l8702/ipod6g/videoout-6g.c | 923 ++++++++++++++++++ firmware/target/arm/s5l8702/lcd-s5l8702.c | 60 ++ firmware/target/arm/s5l8702/lcd-s5l8702.h | 5 + 9 files changed, 1187 insertions(+) create mode 100644 firmware/export/videoout.h create mode 100644 firmware/target/arm/s5l8702/ipod6g/videoout-6g.c diff --git a/firmware/SOURCES b/firmware/SOURCES index 3a5f562bc9..8b524edcc4 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1670,6 +1670,9 @@ target/arm/s5l8702/ipodnano4g/cscodec-nano4g.c #ifdef IPOD_6G target/arm/s5l8702/ipod6g/lcd-6g.c +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) +target/arm/s5l8702/ipod6g/videoout-6g.c +#endif target/arm/s5l8702/ipod6g/backlight-6g.c target/arm/s5l8702/ipod6g/piezo-6g.c target/arm/s5l8702/ipod6g/pmu-6g.c diff --git a/firmware/export/config/ipod6g.h b/firmware/export/config/ipod6g.h index 3980eaa483..a3ade43de5 100644 --- a/firmware/export/config/ipod6g.h +++ b/firmware/export/config/ipod6g.h @@ -90,6 +90,11 @@ /* Define this if your LCD can be enabled/disabled */ #define HAVE_LCD_ENABLE +/* Define this if the target supports composite video output. */ +#ifndef SIMULATOR +#define HAVE_COMPOSITE_VIDEO_OUT +#endif + /* Define this if your LCD can be put to sleep. HAVE_LCD_ENABLE should be defined as well. */ #ifndef BOOTLOADER diff --git a/firmware/export/s5l87xx.h b/firmware/export/s5l87xx.h index 3b13fd72e2..feb220286a 100644 --- a/firmware/export/s5l87xx.h +++ b/firmware/export/s5l87xx.h @@ -1485,6 +1485,79 @@ #define LCD_DRV_RST (*(REG32_PTR_T)(LCD_BASE + 0x28)) /* Reset drive signal */ #define LCD_WDATA (*(REG32_PTR_T)(LCD_BASE + 0x40)) /* Write data register (0x40...0x5C) FIXME */ +#if CONFIG_CPU == S5L8702 +/* Composite video pipeline: video processor, mixer/router, and encoder. */ +#define SVID_COMPOSITOR_BASE 0x39100000u +#define SVID_ROUTER_BASE 0x39200000u +#define SVID_ENCODER_BASE 0x39300000u + +#define SVID_REG(base, offset) \ + (*(REG32_PTR_T)((uintptr_t)(base) + (offset))) + +#define SVID_VP_ENABLE 0x000u +#define SVID_VP_MODE 0x010u +#define SVID_VP_PLANE0_PTR 0x028u +#define SVID_VP_PLANE2_PTR 0x02cu +#define SVID_VP_PLANE1_PTR 0x030u +#define SVID_VP_UNUSED_PTR 0x034u +#define SVID_VP_IMG_WIDTH 0x03cu +#define SVID_VP_IMG_HEIGHT 0x040u +#define SVID_VP_SRC_H_POS 0x044u +#define SVID_VP_SRC_V_POS 0x048u +#define SVID_VP_SRC_WIDTH 0x04cu +#define SVID_VP_SRC_HEIGHT 0x050u +#define SVID_VP_DST_H_POS 0x054u +#define SVID_VP_DST_V_POS 0x058u +#define SVID_VP_DST_WIDTH 0x05cu +#define SVID_VP_DST_HEIGHT 0x060u +#define SVID_VP_H_RATIO 0x064u +#define SVID_VP_V_RATIO 0x068u +#define SVID_VP_PLANE_MODE 0x3c0u +#define SVID_VP_LUMA_SPAN 0x3c4u +#define SVID_VP_CHROMA_SPAN 0x3c8u +#define SVID_VP_ENDIAN_MODE 0x3ccu + +#define SVID_VP_ENABLE_ON (1u << 0) +#define SVID_VP_MODE_RETAIL_FMT8 0u +#define SVID_VP_PLANE_PLANAR 1u +#define SVID_VP_ENDIAN_LITTLE (1u << 0) + +#define SVID_MXR_STATUS 0x000u +#define SVID_MXR_CONFIG 0x004u +#define SVID_MXR_VIDEO_CONFIG 0x008u +#define SVID_MXR_GRAPHIC0_CONFIG 0x00cu +#define SVID_MXR_GRAPHIC0_BASE 0x010u +#define SVID_MXR_GRAPHIC0_POS 0x014u +#define SVID_MXR_GRAPHIC0_SIZE 0x018u +#define SVID_MXR_GRAPHIC_FORMATS 0x040u +#define SVID_MXR_BG_COLOR0 0x048u +#define SVID_MXR_BG_COLOR1 0x04cu +#define SVID_MXR_BG_COLOR2 0x050u +#define SVID_MXR_COMMIT 0x800u + +#define SVID_MXR_STATUS_IDLE (1u << 1) +#define SVID_MXR_STATUS_SYNC (1u << 2) +#define SVID_MXR_STATUS_RUN (1u << 0) +#define SVID_MXR_GRAPHICS_ENABLE (1u << 3) +#define SVID_MXR_VIDEO_ENABLE (1u << 4) +#define SVID_MXR_GRAPHIC4_ENABLE (1u << 5) +#define SVID_MXR_SD_SCAN (1u << 1) +#define SVID_MXR_ENABLE_MASK (SVID_MXR_GRAPHICS_ENABLE | \ + SVID_MXR_VIDEO_ENABLE | \ + SVID_MXR_GRAPHIC4_ENABLE) + +#define SVID_SDO_CLOCK 0x000u +#define SVID_SDO_CONFIG 0x008u +#define SVID_SDO_DAC 0x03cu +#define SVID_SDO_SOFTWARE_RESET (1u << 4) +#define SVID_SDO_CLOCK_ON (1u << 0) +#define SVID_SDO_DAC_ALL_ON 7u +#define SVID_SDO_DAC_MUX_MASK (0x3fu << 8) +#define SVID_SDO_COMPONENT (1u << 6) +#define SVID_SDO_PROGRESSIVE (1u << 4) +#define SVID_SDO_STANDARD_MASK 0xfu +#endif /* CONFIG_CPU == S5L8702 */ + /* 27. CLCD CONTROLLER */ #define LCDBASE 0x39200000 diff --git a/firmware/export/videoout.h b/firmware/export/videoout.h new file mode 100644 index 0000000000..4dce7572e5 --- /dev/null +++ b/firmware/export/videoout.h @@ -0,0 +1,50 @@ +/*************************************************************************** + * Composite video output interface. + * + * Copyright (C) 2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ + +#ifndef VIDEOOUT_H +#define VIDEOOUT_H + +#include + +enum videoout_mode +{ + VIDEOOUT_OFF = 0, + VIDEOOUT_AUTO, + VIDEOOUT_ON, +}; + +enum videoout_accessory +{ + VIDEOOUT_ACCESSORY_NONE = 0, + VIDEOOUT_ACCESSORY_PENDING, + VIDEOOUT_ACCESSORY_VIDEO, + VIDEOOUT_ACCESSORY_OTHER, +}; + +bool videoout_disable(void); +bool videoout_active(void); +bool videoout_lcd_clock_required(void); +bool videoout_mirror_yuv420(const unsigned char *luma, + const unsigned char *cb, + const unsigned char *cr, + int source_x, int source_y, + int source_stride, + int x, int y, int width, int height); +void videoout_mirror_rgb565(const void *source, int x, int y, + int width, int height, int stride); +void videoout_set_mode(enum videoout_mode mode, const void *framebuffer, + int width, int height); + +/* IRQ-safe: requests, but does not perform, the sleeping ADC transaction. */ +void videoout_request_accessory_identification(void); +void videoout_accessory_state(enum videoout_accessory accessory); + +#endif /* VIDEOOUT_H */ diff --git a/firmware/target/arm/s5l8702/ipod6g/power-6g.c b/firmware/target/arm/s5l8702/ipod6g/power-6g.c index c1ff86092f..8f4c245c23 100644 --- a/firmware/target/arm/s5l8702/ipod6g/power-6g.c +++ b/firmware/target/arm/s5l8702/ipod6g/power-6g.c @@ -25,11 +25,18 @@ #include "panic.h" #include "pmu-target.h" #include "usb_core.h" /* for usb_charging_maxcurrent_change */ +#ifndef BOOTLOADER +#include "videoout.h" +#endif static int idepowered; void power_off(void) { +#ifndef BOOTLOADER + videoout_disable(); +#endif + /* USB inserted or EXTON1 */ pmu_set_wake_condition( PCF5063X_OOCWAKE_EXTON2 | PCF5063X_OOCWAKE_EXTON1); diff --git a/firmware/target/arm/s5l8702/ipod6g/serial-6g.c b/firmware/target/arm/s5l8702/ipod6g/serial-6g.c index c9e8247144..d1c1cf87c7 100644 --- a/firmware/target/arm/s5l8702/ipod6g/serial-6g.c +++ b/firmware/target/arm/s5l8702/ipod6g/serial-6g.c @@ -106,6 +106,9 @@ void serial_setup(void) #include "kernel.h" #include "pmu-target.h" #include "iap.h" +#ifndef BOOTLOADER +#include "videoout.h" +#endif static enum { ABR_STATUS_LAUNCHED, /* ST_SYNC */ @@ -115,13 +118,58 @@ static enum { static int bitrate = 0; static bool acc_plugged = false; +static unsigned int acc_absent_ticks; +#ifndef BOOTLOADER +static long videoout_identify_tick; +static bool videoout_identified; +static enum videoout_accessory videoout_accessory = + VIDEOOUT_ACCESSORY_NONE; + +/* The Philips is a silent legacy dock. Do not request identification during + * the connector and iAP startup window: the physically qualified downstream + * build gives silent docks ten seconds before enabling video. */ +#define VIDEOOUT_IDENTIFY_TICKS (10 * HZ) + +static void serial_videoout_publish(enum videoout_accessory accessory) +{ + if (videoout_accessory != accessory) + { + videoout_accessory = accessory; + videoout_accessory_state(accessory); + } +} +#endif + +/* A seated 30-pin accessory can briefly lose its presence contact without + * leaving the connector. Requiring 250 ms of continuous absence avoids + * tearing down and immediately reopening the serial/video session on contact + * bounce while adding only a bounded delay to a real unplug. */ +#define ACCESSORY_REMOVE_DEBOUNCE_TICKS MAX(1, (HZ + 3) / 4) static void serial_acc_tick(void) { bool plugged = pmu_accessory_present(); + + if (acc_plugged && !plugged) + { + if (++acc_absent_ticks < ACCESSORY_REMOVE_DEBOUNCE_TICKS) + return; + } + else + { + acc_absent_ticks = 0; + } + if (acc_plugged != plugged) { acc_plugged = plugged; +#ifndef BOOTLOADER + videoout_identify_tick = current_tick; + videoout_identified = false; + serial_videoout_publish(acc_plugged ? + VIDEOOUT_ACCESSORY_PENDING : + VIDEOOUT_ACCESSORY_NONE); +#endif if (acc_plugged) { uartc_open(ser_port.uartc); @@ -139,6 +187,19 @@ static void serial_acc_tick(void) uartc_close(ser_port.uartc); } } + +#ifndef BOOTLOADER + if (acc_plugged && !videoout_identified && + !TIME_BEFORE(current_tick, + videoout_identify_tick + VIDEOOUT_IDENTIFY_TICKS)) + { + videoout_identified = true; + /* A tick task runs in the timer interrupt. The actual ADC read takes + * a mutex and sleeps for its bias-settle interval, so it must be + * deferred to the normal LCD/settings context. */ + videoout_request_accessory_identification(); + } +#endif } void serial_setup(void) diff --git a/firmware/target/arm/s5l8702/ipod6g/videoout-6g.c b/firmware/target/arm/s5l8702/ipod6g/videoout-6g.c new file mode 100644 index 0000000000..6c45cedc8e --- /dev/null +++ b/firmware/target/arm/s5l8702/ipod6g/videoout-6g.c @@ -0,0 +1,923 @@ +/*************************************************************************** + * iPod Classic 6G/7G S5L8702 composite video output + * + * Copyright (C) 2026 David Cormier + * + * The register sequence and private planar format were reconstructed from + * Apple's iPod Classic firmware and physically qualified on a Philips + * DCP750/37. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + ****************************************************************************/ + +#include "config.h" +#include "system.h" +#include "kernel.h" +#include "s5l87xx.h" +#include "clocking-s5l8702.h" +#include "lcd-s5l8702.h" +#include "adc-target.h" +#include "pmu-target.h" +#include "videoout.h" +#include + +#include + +/* RetailOS 35.2.0.4 format 8 selects the S5L8702 private planar layout. + * In private plane mode 1, offsets 0x28, 0x2c, and 0x30 carry the three + * YUV420 planes rather than the later public Samsung top/bottom-field + * layout. */ +/* The S5L8702 mixer predates the public S5P register layout. RetailOS + * 35.2.0.4 writes layer-0 base, position, and dimensions at 0x10, 0x14, and + * 0x18. Offset 0x14 is not a source stride. */ +/* Qualified on the DCP750: a centered 90% NTSC viewport keeps every + * Rockbox UI pixel inside the panel's composite overscan area. */ +#define SVID_UI_DESTINATION_X 36u +#define SVID_UI_DESTINATION_Y 24u +#define SVID_UI_DESTINATION_WIDTH 648u +#define SVID_UI_DESTINATION_HEIGHT 432u +/* RetailOS's nominal 4:3 movie path supplies private format 8 with a native + * 320x240 planar source. The VP, not software, scales that source into the + * 720x480 NTSC destination. This exact source geometry matters: the DCP750 + * repeatedly rejected the otherwise equivalent 640x240 software-expanded + * layout at logical source row 120 regardless of image/source height values. + * One Cb and one Cr sample cover a 2x2 block of native luma samples. */ +#define SVID_PLANAR_Y_WIDTH LCD_WIDTH +#define SVID_PLANAR_Y_HEIGHT LCD_HEIGHT +#define SVID_PLANAR_SOURCE_HEIGHT SVID_PLANAR_Y_HEIGHT +#define SVID_PLANAR_Y_SIZE (SVID_PLANAR_Y_WIDTH * \ + SVID_PLANAR_Y_HEIGHT) +#define SVID_PLANAR_C_WIDTH (SVID_PLANAR_Y_WIDTH / 2) +#define SVID_PLANAR_C_HEIGHT (SVID_PLANAR_Y_HEIGHT / 2) +#define SVID_PLANAR_C_SIZE (SVID_PLANAR_C_WIDTH * \ + SVID_PLANAR_C_HEIGHT) +#define SVID_PLANAR_FRAME_SIZE (SVID_PLANAR_Y_SIZE + \ + 2 * SVID_PLANAR_C_SIZE) + +#define SVID_CLOCK_SOURCE 3 +#define SVID_CLOCK_DIVIDER 4 +#define SVID_CLOCK_POLL_MAX 1000 +#define SVID_POWER_GATE_0 14 +#define SVID_POWER_GATE_1 15 +#define SVID_POWER_GATE_2 16 +#define SVID_POWER_GATE_MASK ((1u << SVID_POWER_GATE_0) | \ + (1u << SVID_POWER_GATE_1) | \ + (1u << SVID_POWER_GATE_2)) + +#define SVID_GPIO_E4_VIDEO 0x000a040fu +#define SVID_GPIO_E4_MASK (0xfu << 16) + +struct svid_regval +{ + uint16_t offset; + uint32_t value; +}; + +static bool svid_active; +static bool svid_layer_active; +static bool svid_layer_planar; +static bool svid_mirror_enabled; +static bool svid_bus_boosted; +/* 96 bytes total. These target-owned tables replace three software + * divisions per mirrored pixel without taking core or playback memory. */ +static uint8_t svid_rgb5_to_8[32]; +static uint8_t svid_rgb6_to_8[64]; +static bool svid_rgb_expansion_ready; +static uint8_t svid_output_framebuffer[SVID_PLANAR_FRAME_SIZE] + CACHEALIGN_ATTR; +static const void *svid_policy_framebuffer; +static int svid_policy_width; +static int svid_policy_height; +static volatile enum videoout_mode svid_mode = VIDEOOUT_OFF; +static volatile enum videoout_accessory svid_accessory = + VIDEOOUT_ACCESSORY_NONE; +static volatile bool svid_policy_pending; +static volatile bool svid_identification_pending; +static bool svid_platform_saved; +static uint16_t svid_saved_clock; +static uint32_t svid_saved_power_gates; +static uint32_t svid_saved_gpio_e4; + +static void svid_apply_policy(void); + +/* Auto remains deliberately narrow: this is the physically observed + * DCP750/37 accessory-identify resistor range. */ +#define VIDEOOUT_DCP750_RID_MIN 500000 +#define VIDEOOUT_DCP750_RID_MAX 575000 + +static void svid_identify_accessory(void) +{ + int resistor; + + if (!svid_identification_pending) + return; + + svid_identification_pending = false; + if (!pmu_accessory_present()) + { + svid_accessory = VIDEOOUT_ACCESSORY_NONE; + return; + } + + /* This function is reached only from normal LCD/settings context. The + * ADC implementation takes a mutex, sleeps for its 50 ms bias-settle + * interval, and yields while converting; none of that is IRQ-safe. */ + resistor = adc_read_accessory_resistor(); + + /* The connector may have been removed while the normal thread slept. */ + if (!pmu_accessory_present()) + svid_accessory = VIDEOOUT_ACCESSORY_NONE; + else if (resistor >= VIDEOOUT_DCP750_RID_MIN && + resistor <= VIDEOOUT_DCP750_RID_MAX) + svid_accessory = VIDEOOUT_ACCESSORY_VIDEO; + else + svid_accessory = VIDEOOUT_ACCESSORY_OTHER; +} + +static void svid_set_bus_boost(bool enable) +{ + if (enable == svid_bus_boosted) + return; + + /* The VP memory reader underruns when Rockbox lowers HClk to 54 MHz. + * Own one normal boost reference for the lifetime of every memory-backed + * layer; this holds HClk at 108 MHz without interfering with other boost + * owners, and the paired release restores normal power policy. */ + cpu_boost(enable); + svid_bus_boosted = enable; +} + +static const struct svid_regval compositor_init[] = { + {0x004, 0}, {0x008, 0}, {0x00c, 0}, + {0x028, 0x08000000}, {0x02c, 0x08000000}, + {0x030, 0x08000000}, {0x034, 0x08000000}, + {0x038, 0x08000000}, + {0x03c, 64}, {0x040, 16}, {0x044, 0}, {0x048, 0}, + {0x04c, 64}, {0x050, 16}, {0x054, 0}, {0x058, 0}, + {0x05c, 64}, {0x060, 16}, {0x064, 512}, {0x068, 512}, + {0x3c0, 1}, {0x3cc, 1}, + {0x06c, 0x00070707}, {0x070, 0x07070707}, + {0x074, 0x07070707}, {0x078, 0x07000000}, + {0x07c, 0x00020405}, {0x080, 0x06060606}, + {0x084, 0x06050504}, {0x088, 0x03020101}, + {0x08c, 0x007a7470}, {0x090, 0x6e6c6b6c}, + {0x094, 0x6c6e7073}, {0x098, 0x76787b7e}, + {0x09c, 0x7f7e7d79}, {0x0a0, 0x726b6359}, + {0x0a4, 0x4f44392e}, {0x0a8, 0x23191008}, + {0x0ec, 0x003d3a38}, {0x0f0, 0x38383839}, + {0x0f4, 0x3a3b3c3d}, {0x0f8, 0x3e3f3f00}, + {0x0fc, 0x7f7e7c76}, {0x100, 0x6f665c51}, + {0x104, 0x463b3025}, {0x108, 0x1b130b05}, + {0x10c, 0x00050b13}, {0x110, 0x1b25303b}, + {0x114, 0x46515c66}, {0x118, 0x6f767c7e}, + {0x11c, 0x00003f3f}, {0x120, 0x3e3d3c3b}, + {0x124, 0x3a393838}, {0x128, 0x38383a3d}, + {0x12c, 0x6b6b6d6f}, {0x130, 0x3336393d}, + {0x134, 0x3f010203}, {0x138, 0x03030202}, + {0x13c, 0xaaa69f95}, {0x140, 0x88786754}, + {0x144, 0x412e1e0f}, {0x148, 0x0279726d}, + {0x200, 1}, {0x20c, 0}, {0x210, 0}, + {0x218, 0x80}, {0x21c, 0x80000080}, + {0x220, 0x80}, {0x224, 0x80}, {0x228, 0x80}, + {0x22c, 0x80}, {0x230, 0x80}, {0x234, 0x80}, + {0x238, 0}, +}; + +static const struct svid_regval router_init[] = { + {SVID_MXR_STATUS, SVID_MXR_STATUS_IDLE | SVID_MXR_STATUS_SYNC}, + {SVID_MXR_CONFIG, 0}, + {SVID_MXR_VIDEO_CONFIG, 0}, + {SVID_MXR_GRAPHIC0_CONFIG, 0}, + {SVID_MXR_GRAPHIC0_BASE, 0}, {SVID_MXR_GRAPHIC0_POS, 0}, + {SVID_MXR_GRAPHIC0_SIZE, 0}, + {0x01c, 0}, {0x020, 0}, {0x024, 0}, {0x028, 0}, {0x02c, 0}, + {0x030, 0}, {0x034, 0}, {0x038, 0}, {0x03c, 0}, + {SVID_MXR_GRAPHIC_FORMATS, 0}, {0x044, 0}, + {SVID_MXR_BG_COLOR0, 0x00108080}, + {SVID_MXR_BG_COLOR1, 0}, {SVID_MXR_BG_COLOR2, 0}, + {0x054, 0}, {0x058, 0}, + {0x080, 0x08440832}, {0x084, 0x3b4dace1}, + {0x088, 0x0e1d13dc}, {SVID_MXR_COMMIT, 1}, +}; + +static const struct svid_regval encoder_ntsc[] = { + {0x00c, 6}, {0x010, 1}, {0x014, 0x0000440c}, + {0x01c, 0x800}, {0x020, 0x800}, {0x024, 0x800}, + {0x028, 0x800}, {0x02c, 0x800}, {0x030, 0x800}, + {0x038, 0}, {0x03c, 0x01000700}, + {0x044, 0}, {0x048, 0}, {0x04c, 0}, {0x050, 0}, + {0x054, 0}, {0x058, 0}, {0x05c, 0}, {0x060, 0}, + {0x064, 0}, {0x068, 0}, {0x06c, 0}, + {0x070, 0x0000025d}, + {0x080, 0}, {0x084, 0}, {0x088, 0}, {0x08c, 0}, + {0x090, 0}, {0x094, 1}, {0x098, 7}, {0x09c, 20}, + {0x0a0, 40}, {0x0a4, 63}, {0x0a8, 82}, {0x0ac, 90}, + {0x0c0, 0}, {0x0c4, 0}, {0x0c8, 0}, {0x0cc, 0}, + {0x0d0, 0}, {0x0d4, 1}, {0x0d8, 9}, {0x0dc, 28}, + {0x0e0, 57}, {0x0e4, 90}, {0x0e8, 116}, {0x0ec, 126}, + {0x0f0, 0}, + {0x100, 0}, {0x104, 0}, {0x108, 0}, {0x10c, 0}, + {0x110, 0}, {0x114, 0}, {0x118, 0}, {0x11c, 0}, + {0x120, 0}, {0x124, 0}, {0x128, 0}, {0x12c, 0}, + {0x130, 0}, {0x134, 0}, {0x138, 0}, {0x13c, 0}, + {0x140, 0}, {0x144, 0}, {0x148, 0}, {0x14c, 0}, + {0x150, 0}, {0x154, 0}, {0x158, 0}, {0x15c, 0}, + {0x184, 0x00800000}, {0x188, 0x00800000}, + {0x18c, 0x80}, {0x190, 0}, {0x194, 0x0000eb10}, + {0x198, 0x02000000}, {0x19c, 0x03ff0200}, + {0x1a0, 0x1ff}, {0x1a4, 0x03ff0000}, + {0x1a8, 0x1ff}, {0x1c0, 17}, + {0x200, 0x00fd00fe}, {0x204, 0}, {0x208, 0x00050004}, + {0x20c, 0xff}, {0x210, 0x00f700fa}, {0x214, 1}, + {0x218, 0x000e000a}, {0x21c, 0x1ff}, + {0x220, 0x01ec01f2}, {0x224, 1}, + {0x228, 0x001d0014}, {0x22c, 0x000001fe}, + {0x230, 0x03d803e4}, {0x234, 2}, {0x238, 0x00380028}, + {0x23c, 0x3fd}, {0x240, 0x03b003c7}, {0x244, 5}, + {0x248, 0x00790056}, {0x24c, 0x000003f6}, + {0x250, 0x072c0766}, {0x254, 27}, + {0x258, 0x028b0265}, {0x25c, 0x04000ecc}, + {0x260, 0}, {0x264, 0}, {0x268, 0}, {0x26c, 0x00011a00}, + {0x280, 0}, {0x3c0, 0}, {0x3c4, 0x00010000}, + {0x3c8, 8}, {0x3cc, 0x00010000}, + {0x3d0, 1}, {0x3d4, 8}, +}; + +static void svid_write_table(uintptr_t base, + const struct svid_regval *table, + unsigned count) +{ + for (unsigned i = 0; i < count; i++) + SVID_REG(base, table[i].offset) = table[i].value; +} + +static bool svid_write_clock(uint16_t value) +{ + volatile uint32_t *reg32 = (volatile uint32_t *) + ((uintptr_t)&CG16_SVID & ~(uintptr_t)3); + unsigned shift = ((uintptr_t)&CG16_SVID & 2u) << 3; + uint32_t preserve = 0xffff0000u >> shift; + + *reg32 = (*reg32 & preserve) | ((uint32_t)value << shift); + for (unsigned i = 0; i < SVID_CLOCK_POLL_MAX; i++) + { + if (CG16_SVID == value) + return true; + udelay(1); + } + return false; +} + +static void svid_platform_restore(void) +{ + if (!svid_platform_saved) + return; + + PCON(14) = (PCON(14) & ~SVID_GPIO_E4_MASK) | svid_saved_gpio_e4; + PWRCON(0) = (PWRCON(0) & ~SVID_POWER_GATE_MASK) | + svid_saved_power_gates; + svid_write_clock(svid_saved_clock); + svid_platform_saved = false; +} + +static bool svid_platform_enable(void) +{ + uint16_t clock_value = + ((SVID_CLOCK_SOURCE & CG16_SEL_MSK) << CG16_SEL_POS) | + (((SVID_CLOCK_DIVIDER - 1) & CG16_DIV1_MSK) << CG16_DIV1_POS); + + svid_saved_clock = CG16_SVID; + svid_saved_power_gates = PWRCON(0) & SVID_POWER_GATE_MASK; + svid_saved_gpio_e4 = PCON(14) & SVID_GPIO_E4_MASK; + svid_platform_saved = true; + + /* Stock firmware selects PLL2 with a divide-by-four SVID clock. */ + if (!svid_write_clock(clock_value)) + { + svid_platform_restore(); + return false; + } + + clockgate_enable(SVID_POWER_GATE_0, true); + clockgate_enable(SVID_POWER_GATE_1, true); + clockgate_enable(SVID_POWER_GATE_2, true); + lcd_videoout_clock_acquire(); + GPIOCMD = SVID_GPIO_E4_VIDEO; + return true; +} + +static void svid_begin_reset(void) +{ + SVID_REG(SVID_COMPOSITOR_BASE, 0) &= 2; + svid_write_table(SVID_COMPOSITOR_BASE, compositor_init, + ARRAYLEN(compositor_init)); + + /* RetailOS initializes compositor, encoder, then output mixer. */ + SVID_REG(SVID_ENCODER_BASE, SVID_SDO_CLOCK) = + SVID_SDO_SOFTWARE_RESET; +} + +static void svid_finish_reset(void) +{ + SVID_REG(SVID_ENCODER_BASE, SVID_SDO_CLOCK) = 0; + SVID_REG(SVID_ENCODER_BASE, 0x180) &= ~0x1fu; + SVID_REG(SVID_ENCODER_BASE, 0x180) |= 0x10; + svid_write_table(SVID_ENCODER_BASE, encoder_ntsc, + ARRAYLEN(encoder_ntsc)); + svid_write_table(SVID_ROUTER_BASE, router_init, ARRAYLEN(router_init)); +} + +static void svid_select_composite(void) +{ + uint32_t timing = 0x792; + uint32_t reg = SVID_REG(SVID_ENCODER_BASE, SVID_SDO_CONFIG); + + /* RetailOS 35.2.0.4 does these as two adjacent operations in its output + * setup: clear mixer bit 2 for NTSC, then set mixer bit 1 before loading + * the interlaced SDO timing table. Every qualified failing build reached + * the mixer as C=0x10 instead of stock C=0x12 and presented only the + * first 120-line field before repeating its final row. */ + SVID_REG(SVID_ROUTER_BASE, SVID_MXR_CONFIG) = + (SVID_REG(SVID_ROUTER_BASE, SVID_MXR_CONFIG) & ~4u) | + SVID_MXR_SD_SCAN; + SVID_REG(SVID_ENCODER_BASE, 0x34) = 0; + /* RetailOS selects the dock's composite path with DAC mux values 1, 0, + * and 2. Clearing these bits produced sync with a green no-input field. */ + reg &= ~(SVID_SDO_DAC_MUX_MASK | SVID_SDO_COMPONENT | + SVID_SDO_PROGRESSIVE | SVID_SDO_STANDARD_MASK); + SVID_REG(SVID_ENCODER_BASE, SVID_SDO_CONFIG) = reg | 0x1200u; + + if (SVID_REG(0x3f000000u, 4) & 0x100u) + { + SVID_REG(SVID_ENCODER_BASE, 0x28) = timing + 0x23; + SVID_REG(SVID_ENCODER_BASE, 0x2c) = timing + 0x6d; + SVID_REG(SVID_ENCODER_BASE, 0x30) = timing; + } + else + { + SVID_REG(SVID_ENCODER_BASE, 0x28) = timing | (timing >> 7); + SVID_REG(SVID_ENCODER_BASE, 0x2c) = timing + 0x27; + SVID_REG(SVID_ENCODER_BASE, 0x30) = timing + 8; + } + +} + +static void svid_start_pipeline(void) +{ + SVID_REG(SVID_ENCODER_BASE, SVID_SDO_DAC) |= SVID_SDO_DAC_ALL_ON; + SVID_REG(SVID_ENCODER_BASE, SVID_SDO_CLOCK) |= SVID_SDO_CLOCK_ON; + SVID_REG(SVID_ROUTER_BASE, SVID_MXR_STATUS) |= SVID_MXR_STATUS_RUN; +} + +static void svid_mixer_commit(void) +{ + SVID_REG(SVID_ROUTER_BASE, SVID_MXR_COMMIT) = 1; +} + +static void svid_init_rgb_expansion(void) +{ + if (svid_rgb_expansion_ready) + return; + + for (unsigned value = 0; value < ARRAYLEN(svid_rgb5_to_8); value++) + svid_rgb5_to_8[value] = value * 255 / 31; + + for (unsigned value = 0; value < ARRAYLEN(svid_rgb6_to_8); value++) + svid_rgb6_to_8[value] = value * 255 / 63; + + svid_rgb_expansion_ready = true; +} + +static uint32_t svid_rgb565_to_ycbcr(uint16_t pixel) +{ + int red = svid_rgb5_to_8[(pixel >> 11) & 0x1f]; + int green = svid_rgb6_to_8[(pixel >> 5) & 0x3f]; + int blue = svid_rgb5_to_8[pixel & 0x1f]; + int y; + int cb; + int cr; + + /* ITU-R BT.601 limited-range values, matching the SD mixer. */ + /* The exhaustive color gate proves these limited-range expressions stay + * inside byte range for all 65,536 RGB565 inputs. */ + y = ((66 * red + 129 * green + 25 * blue + 128) >> 8) + 16; + cb = ((-38 * red - 74 * green + 112 * blue + 128) >> 8) + 128; + cr = ((112 * red - 94 * green - 18 * blue + 128) >> 8) + 128; + return (uint32_t)y | ((uint32_t)cb << 8) | ((uint32_t)cr << 16); +} + +static void svid_copy_framebuffer_planar(const uint16_t *source, + int width, int height) +{ + uint8_t *luma = (uint8_t *)svid_output_framebuffer; + uint8_t *cb = luma + SVID_PLANAR_Y_SIZE; + uint8_t *cr = cb + SVID_PLANAR_C_SIZE; + + memset(luma, 16, SVID_PLANAR_Y_SIZE); + memset(cb, 128, SVID_PLANAR_C_SIZE); + memset(cr, 128, SVID_PLANAR_C_SIZE); + + for (int y = 0; y < height; y += 2) + { + uint8_t *luma0 = luma + y * SVID_PLANAR_Y_WIDTH; + uint8_t *cb0 = cb + (y / 2) * SVID_PLANAR_C_WIDTH; + uint8_t *cr0 = cr + (y / 2) * SVID_PLANAR_C_WIDTH; + int y1 = MIN(y + 1, height - 1); + uint8_t *luma1 = luma + y1 * SVID_PLANAR_Y_WIDTH; + const uint16_t *source0 = source + y * width; + const uint16_t *source1 = source + y1 * width; + + for (int x = 0; x < width; x += 2) + { + int x1 = MIN(x + 1, width - 1); + uint32_t pixel00 = svid_rgb565_to_ycbcr(source0[x]); + uint32_t pixel01 = svid_rgb565_to_ycbcr(source0[x1]); + uint32_t pixel10 = svid_rgb565_to_ycbcr(source1[x]); + uint32_t pixel11 = svid_rgb565_to_ycbcr(source1[x1]); + + luma0[x] = pixel00; + luma0[x1] = pixel01; + luma1[x] = pixel10; + luma1[x1] = pixel11; + cb0[x / 2] = (((pixel00 >> 8) & 0xff) + + ((pixel01 >> 8) & 0xff) + + ((pixel10 >> 8) & 0xff) + + ((pixel11 >> 8) & 0xff) + 2) >> 2; + cr0[x / 2] = (((pixel00 >> 16) & 0xff) + + ((pixel01 >> 16) & 0xff) + + ((pixel10 >> 16) & 0xff) + + ((pixel11 >> 16) & 0xff) + 2) >> 2; + } + } + + commit_dcache_range(luma, SVID_PLANAR_FRAME_SIZE); +} + +static bool svid_enable_sync(void) +{ + svid_init_rgb_expansion(); + + if (svid_active) + return true; + + int oldlevel = disable_irq_save(); + if (!svid_platform_enable()) + { + restore_irq(oldlevel); + return false; + } + svid_begin_reset(); + + /* The reset wait is almost the 44.1 kHz PCM DMA emergency-buffer + * duration. Service DMA during the wait, but keep every SVID register + * write in the original atomic initialization sequence. */ + restore_irq(oldlevel); + udelay(10000); + + oldlevel = disable_irq_save(); + svid_finish_reset(); + svid_select_composite(); + svid_start_pipeline(); + restore_irq(oldlevel); + + svid_active = true; + svid_layer_active = false; + svid_layer_planar = false; + svid_mirror_enabled = false; + return true; +} + +static bool svid_show_framebuffer(const void *framebuffer, + int width, int height) +{ + if (framebuffer == NULL || width != LCD_WIDTH || height != LCD_HEIGHT) + return false; + + if (!svid_enable_sync()) + return false; + + svid_set_bus_boost(true); + + int oldlevel = disable_irq_save(); + SVID_REG(SVID_ROUTER_BASE, SVID_MXR_CONFIG) &= + ~SVID_MXR_ENABLE_MASK; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_ENABLE) &= + ~SVID_VP_ENABLE_ON; + svid_mixer_commit(); + restore_irq(oldlevel); + + svid_copy_framebuffer_planar(framebuffer, width, height); + + oldlevel = disable_irq_save(); + + uintptr_t luma = (uintptr_t)svid_output_framebuffer; + uintptr_t cb = luma + SVID_PLANAR_Y_SIZE; + uintptr_t cr = cb + SVID_PLANAR_C_SIZE; + + /* Exact RetailOS private format-8 setup: descriptor 0 -> 0x28, + * descriptor 2 -> 0x2c, descriptor 1 -> 0x30, zero -> 0x34, spans width + * and width/2, and plane mode 1. Physical DCP750 color qualification + * fixes this private ordering as Y, Cb, and Cr for our generated frame. */ + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_MODE) = + SVID_VP_MODE_RETAIL_FMT8; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_PLANE0_PTR) = luma; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_PLANE2_PTR) = cb; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_UNUSED_PTR) = 0; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_PLANE1_PTR) = cr; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_IMG_WIDTH) = + SVID_PLANAR_Y_WIDTH; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_IMG_HEIGHT) = + SVID_PLANAR_Y_HEIGHT; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_SRC_H_POS) = 0; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_SRC_V_POS) = 0; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_SRC_WIDTH) = + SVID_PLANAR_Y_WIDTH; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_SRC_HEIGHT) = + SVID_PLANAR_SOURCE_HEIGHT; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_DST_H_POS) = + SVID_UI_DESTINATION_X; + /* RetailOS's direct geometry path writes caller-provided destination Y + * and height verbatim on this exact SoC. + * Applying the later S5P driver's field-line /2 conversion produces the + * physically observed upper-half-only picture. */ + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_DST_V_POS) = + SVID_UI_DESTINATION_Y; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_DST_WIDTH) = + SVID_UI_DESTINATION_WIDTH; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_DST_HEIGHT) = + SVID_UI_DESTINATION_HEIGHT; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_H_RATIO) = + ((SVID_PLANAR_Y_WIDTH << 12) / SVID_UI_DESTINATION_WIDTH) >> 3; + /* Destination geometry remains in RetailOS full-frame coordinates. */ + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_V_RATIO) = + ((SVID_PLANAR_Y_HEIGHT << 12) / SVID_UI_DESTINATION_HEIGHT) >> 4; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_PLANE_MODE) = + SVID_VP_PLANE_PLANAR; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_LUMA_SPAN) = + SVID_PLANAR_Y_WIDTH; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_CHROMA_SPAN) = + SVID_PLANAR_C_WIDTH; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_ENDIAN_MODE) = + SVID_VP_ENDIAN_LITTLE; + + SVID_REG(SVID_ROUTER_BASE, SVID_MXR_VIDEO_CONFIG) = 0; + SVID_REG(SVID_ROUTER_BASE, SVID_MXR_BG_COLOR0) = 0x00108080u; + SVID_REG(SVID_ROUTER_BASE, SVID_MXR_BG_COLOR1) = 0x00108080u; + SVID_REG(SVID_ROUTER_BASE, SVID_MXR_BG_COLOR2) = 0x00108080u; + SVID_REG(SVID_COMPOSITOR_BASE, SVID_VP_ENABLE) |= + SVID_VP_ENABLE_ON; + SVID_REG(SVID_ROUTER_BASE, SVID_MXR_CONFIG) = + (SVID_REG(SVID_ROUTER_BASE, SVID_MXR_CONFIG) & + ~SVID_MXR_ENABLE_MASK) | SVID_MXR_VIDEO_ENABLE; + SVID_REG(SVID_ROUTER_BASE, SVID_MXR_STATUS) = + SVID_MXR_STATUS_IDLE | SVID_MXR_STATUS_SYNC | SVID_MXR_STATUS_RUN; + svid_mixer_commit(); + + restore_irq(oldlevel); + svid_layer_planar = true; + svid_mirror_enabled = true; + svid_layer_active = true; + return true; +} + +static bool svid_stop_block(uintptr_t base) +{ + SVID_REG(base, 0) &= ~1u; + for (unsigned i = 0; i < 10; i++) + { + sleep(1); + if (SVID_REG(base, 0) & 2) + return true; + } + return false; +} + +bool videoout_active(void) +{ + if (svid_policy_pending) + svid_apply_policy(); + + return svid_active && svid_layer_active; +} + +bool videoout_lcd_clock_required(void) +{ + return svid_platform_saved; +} + +static void svid_commit_planar_rect(int x, int y, int width, int height) +{ + uint8_t *luma = (uint8_t *)svid_output_framebuffer; + uint8_t *cb = luma + SVID_PLANAR_Y_SIZE; + uint8_t *cr = cb + SVID_PLANAR_C_SIZE; + int first_chroma_column = x / 2; + int last_chroma_column = (x + width - 1) / 2; + int first_chroma_row = y / 2; + int last_chroma_row = (y + height - 1) / 2; + unsigned luma_span = (height - 1) * SVID_PLANAR_Y_WIDTH + width; + unsigned chroma_width = last_chroma_column - first_chroma_column + 1; + unsigned chroma_span = + (last_chroma_row - first_chroma_row) * SVID_PLANAR_C_WIDTH + + chroma_width; + + /* One clean per touched plane replaces a clean/barrier for every row. + * The spans include untouched bytes between partial rows but never leave + * the fixed output buffer. */ + commit_dcache_range(luma + y * SVID_PLANAR_Y_WIDTH + x, luma_span); + commit_dcache_range(cb + first_chroma_row * SVID_PLANAR_C_WIDTH + + first_chroma_column, chroma_span); + commit_dcache_range(cr + first_chroma_row * SVID_PLANAR_C_WIDTH + + first_chroma_column, chroma_span); +} + +static void svid_copy_yuv_plane(uint8_t *destination, + unsigned destination_stride, + const uint8_t *source, + unsigned source_stride, + unsigned width, unsigned height) +{ + if (width == destination_stride && width == source_stride) + { + memcpy(destination, source, width * height); + return; + } + + while (height-- > 0) + { + memcpy(destination, source, width); + destination += destination_stride; + source += source_stride; + } +} + +bool videoout_mirror_yuv420(const unsigned char *source_luma, + const unsigned char *source_cb, + const unsigned char *source_cr, + int source_x, int source_y, + int source_stride, + int x, int y, int width, int height) +{ + uint8_t *luma = (uint8_t *)svid_output_framebuffer; + uint8_t *cb = luma + SVID_PLANAR_Y_SIZE; + uint8_t *cr = cb + SVID_PLANAR_C_SIZE; + + if (!videoout_active() || !svid_mirror_enabled || + !svid_layer_planar || source_luma == NULL || source_cb == NULL || + source_cr == NULL || source_x < 0 || source_y < 0 || + source_stride < width || x < 0 || y < 0 || width <= 0 || + height <= 0 || x + width > LCD_WIDTH || y + height > LCD_HEIGHT || + source_x + width > source_stride || + ((source_x | source_y | source_stride | x | y | width | height) & 1)) + return false; + + source_luma += source_y * source_stride + source_x; + source_cb += (source_y / 2) * (source_stride / 2) + source_x / 2; + source_cr += (source_y / 2) * (source_stride / 2) + source_x / 2; + + svid_copy_yuv_plane(luma + y * SVID_PLANAR_Y_WIDTH + x, + SVID_PLANAR_Y_WIDTH, source_luma, source_stride, + width, height); + svid_copy_yuv_plane(cb + (y / 2) * SVID_PLANAR_C_WIDTH + x / 2, + SVID_PLANAR_C_WIDTH, source_cb, source_stride / 2, + width / 2, height / 2); + svid_copy_yuv_plane(cr + (y / 2) * SVID_PLANAR_C_WIDTH + x / 2, + SVID_PLANAR_C_WIDTH, source_cr, source_stride / 2, + width / 2, height / 2); + svid_commit_planar_rect(x, y, width, height); + return true; +} + +static void svid_mirror_rgb565_planar_even(const uint16_t *source, + int x, int y, int width, + int height, int stride) +{ + uint8_t *luma = (uint8_t *)svid_output_framebuffer; + uint8_t *cb = luma + SVID_PLANAR_Y_SIZE; + uint8_t *cr = cb + SVID_PLANAR_C_SIZE; + + for (int row = 0; row < height; row += 2) + { + const uint16_t *source0 = source + row * stride; + const uint16_t *source1 = source0 + stride; + uint8_t *luma0 = luma + (y + row) * SVID_PLANAR_Y_WIDTH + x; + uint8_t *luma1 = luma0 + SVID_PLANAR_Y_WIDTH; + uint8_t *cb0 = cb + ((y + row) / 2) * SVID_PLANAR_C_WIDTH + x / 2; + uint8_t *cr0 = cr + ((y + row) / 2) * SVID_PLANAR_C_WIDTH + x / 2; + + for (int column = 0; column < width; column += 2) + { + uint32_t pixel00 = svid_rgb565_to_ycbcr(source0[column]); + uint32_t pixel01 = svid_rgb565_to_ycbcr(source0[column + 1]); + uint32_t pixel10 = svid_rgb565_to_ycbcr(source1[column]); + uint32_t pixel11 = svid_rgb565_to_ycbcr(source1[column + 1]); + + luma0[column] = pixel00; + luma0[column + 1] = pixel01; + luma1[column] = pixel10; + luma1[column + 1] = pixel11; + cb0[column / 2] = (((pixel00 >> 8) & 0xff) + + ((pixel01 >> 8) & 0xff) + + ((pixel10 >> 8) & 0xff) + + ((pixel11 >> 8) & 0xff) + 2) >> 2; + cr0[column / 2] = (((pixel00 >> 16) & 0xff) + + ((pixel01 >> 16) & 0xff) + + ((pixel10 >> 16) & 0xff) + + ((pixel11 >> 16) & 0xff) + 2) >> 2; + } + } +} + +static void svid_mirror_rgb565_planar(const uint16_t *source, int x, int y, + int width, int height, int stride) +{ + uint8_t *luma = (uint8_t *)svid_output_framebuffer; + uint8_t *cb = luma + SVID_PLANAR_Y_SIZE; + uint8_t *cr = cb + SVID_PLANAR_C_SIZE; + + /* LCD and decoded-video frames use even 4:2:0 rectangles. Convert each + * source pixel exactly once, writing its luma and contributing its chroma + * in the same pass. */ + if (((x | y | width | height) & 1) == 0) + { + svid_mirror_rgb565_planar_even(source, x, y, width, height, stride); + svid_commit_planar_rect(x, y, width, height); + return; + } + + for (int row = 0; row < height; row++) + { + uint8_t *luma0 = luma + (y + row) * SVID_PLANAR_Y_WIDTH + x; + const uint16_t *source_row = source + row * stride; + + for (int column = 0; column < width; column++) + { + luma0[column] = svid_rgb565_to_ycbcr(source_row[column]); + } + } + + int first_chroma_column = x / 2; + int last_chroma_column = (x + width - 1) / 2; + int first_chroma_row = y / 2; + int last_chroma_row = (y + height - 1) / 2; + for (int chroma_row = first_chroma_row; + chroma_row <= last_chroma_row; chroma_row++) + { + int source_y0 = MAX(chroma_row * 2, y) - y; + int source_y1 = MIN(chroma_row * 2 + 1, y + height - 1) - y; + const uint16_t *source_row0 = source + source_y0 * stride; + const uint16_t *source_row1 = source + source_y1 * stride; + uint8_t *cb0 = cb + chroma_row * SVID_PLANAR_C_WIDTH + + first_chroma_column; + uint8_t *cr0 = cr + chroma_row * SVID_PLANAR_C_WIDTH + + first_chroma_column; + + for (int chroma_column = first_chroma_column; + chroma_column <= last_chroma_column; chroma_column++) + { + int source_x0 = MAX(chroma_column * 2, x) - x; + int source_x1 = MIN(chroma_column * 2 + 1, + x + width - 1) - x; + int output_column = chroma_column - first_chroma_column; + uint32_t pixel00 = + svid_rgb565_to_ycbcr(source_row0[source_x0]); + uint32_t pixel01 = + svid_rgb565_to_ycbcr(source_row0[source_x1]); + uint32_t pixel10 = + svid_rgb565_to_ycbcr(source_row1[source_x0]); + uint32_t pixel11 = + svid_rgb565_to_ycbcr(source_row1[source_x1]); + + cb0[output_column] = (((pixel00 >> 8) & 0xff) + + ((pixel01 >> 8) & 0xff) + + ((pixel10 >> 8) & 0xff) + + ((pixel11 >> 8) & 0xff) + 2) >> 2; + cr0[output_column] = (((pixel00 >> 16) & 0xff) + + ((pixel01 >> 16) & 0xff) + + ((pixel10 >> 16) & 0xff) + + ((pixel11 >> 16) & 0xff) + 2) >> 2; + } + } + + svid_commit_planar_rect(x, y, width, height); +} + +void videoout_mirror_rgb565(const void *source, int x, int y, + int width, int height, int stride) +{ + const uint16_t *src = source; + + if (!videoout_active() || !svid_mirror_enabled || source == NULL || + !svid_layer_planar || x < 0 || y < 0 || width <= 0 || height <= 0 || + stride < width || x + width > LCD_WIDTH || + y + height > LCD_HEIGHT) + return; + + svid_mirror_rgb565_planar(src, x, y, width, height, stride); +} + +bool videoout_disable(void) +{ + if (!svid_active) + { + svid_set_bus_boost(false); + return true; + } + + SVID_REG(SVID_ROUTER_BASE, SVID_MXR_CONFIG) &= + ~SVID_MXR_ENABLE_MASK; + SVID_REG(SVID_ENCODER_BASE, 0x03c) &= ~0xfu; + bool clean = svid_stop_block(SVID_ENCODER_BASE); + clean = svid_stop_block(SVID_ROUTER_BASE) && clean; + clean = svid_stop_block(SVID_COMPOSITOR_BASE) && clean; + + int oldlevel = disable_irq_save(); + svid_platform_restore(); + restore_irq(oldlevel); + + svid_active = false; + svid_layer_active = false; + svid_layer_planar = false; + svid_mirror_enabled = false; + lcd_videoout_clock_release(); + svid_set_bus_boost(false); + return clean; +} + +static void svid_apply_policy(void) +{ + /* Off is fully dormant: do not even perform the one-time accessory ADC + * transaction. A deferred request remains available if the user later + * selects Auto or On while the same dock is still connected. */ + if (svid_mode != VIDEOOUT_OFF) + svid_identify_accessory(); + + /* Auto requires the qualified DCP750 signature. On accepts a dock only + * after accessory identification has completed; PENDING must not bypass + * the startup window and start SVID while the connector/iAP state is still + * settling. NONE must never retain CPU/LCD clocks while undocked. */ + bool enable = + (svid_mode == VIDEOOUT_AUTO && + svid_accessory == VIDEOOUT_ACCESSORY_VIDEO) || + (svid_mode == VIDEOOUT_ON && + (svid_accessory == VIDEOOUT_ACCESSORY_VIDEO || + svid_accessory == VIDEOOUT_ACCESSORY_OTHER)); + + svid_policy_pending = false; + + if (enable) + { + if ((!svid_active || !svid_layer_active) && + svid_policy_framebuffer != NULL) + svid_show_framebuffer(svid_policy_framebuffer, + svid_policy_width, + svid_policy_height); + } + else if (svid_active) + { + videoout_disable(); + } +} + +void videoout_request_accessory_identification(void) +{ + /* Called by the serial tick task: stores only, no ADC/I2C or SVID work. */ + svid_identification_pending = true; + if (svid_mode != VIDEOOUT_OFF) + svid_policy_pending = true; +} + +void videoout_set_mode(enum videoout_mode mode, + const void *framebuffer, + int width, int height) +{ + if (mode > VIDEOOUT_ON) + mode = VIDEOOUT_OFF; + + svid_mode = mode; + svid_policy_framebuffer = framebuffer; + svid_policy_width = width; + svid_policy_height = height; + svid_policy_pending = true; + svid_apply_policy(); +} + +void videoout_accessory_state(enum videoout_accessory accessory) +{ + if (accessory > VIDEOOUT_ACCESSORY_OTHER) + accessory = VIDEOOUT_ACCESSORY_OTHER; + + if (accessory != VIDEOOUT_ACCESSORY_PENDING) + svid_identification_pending = false; + + if (svid_accessory != accessory) + { + svid_accessory = accessory; + svid_policy_pending = true; + } +} diff --git a/firmware/target/arm/s5l8702/lcd-s5l8702.c b/firmware/target/arm/s5l8702/lcd-s5l8702.c index 4e8e8af1e3..1c87d37da0 100644 --- a/firmware/target/arm/s5l8702/lcd-s5l8702.c +++ b/firmware/target/arm/s5l8702/lcd-s5l8702.c @@ -37,6 +37,9 @@ #include "dma-s5l8702.h" #include "lcd-s5l8702.h" #include "lcd-target.h" +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) +#include "videoout.h" +#endif // TODO TODO TODO: HAVE_LCD_ENABLE @@ -185,6 +188,10 @@ static struct dmac_ch_cfg lcd_dma_ch_cfg = /*** clocks ***/ +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) +static bool lcd_clocks_requested = true; +#endif + // TODO: In mks5lboot --mkraw put a command to specify the address of the binary, // for example --address 0x6000, it must be greater than 0x310 which would be the default address, // pass this address (0x310..128Kb) in the dfu_options flag @@ -192,12 +199,34 @@ static struct dmac_ch_cfg lcd_dma_ch_cfg = // TODO: to lcd-target.c static void lcd_target_enable_clocks(bool enable) { +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) + lcd_clocks_requested = enable; + + /* The composite mixer shares this LCD AHB gate. Keep the controller + * clocked while video output is scanning even if the backlight sleeps. */ + if (!enable && videoout_lcd_clock_required()) + return; +#endif + clockgate_enable(CLOCKGATE_LCD, enable); #ifdef IPOD_NANO4G clockgate_enable(CLOCKGATE_LCD_2, enable); #endif } +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) +void lcd_videoout_clock_acquire(void) +{ + clockgate_enable(CLOCKGATE_LCD, true); +} + +void lcd_videoout_clock_release(void) +{ + if (!lcd_clocks_requested) + clockgate_enable(CLOCKGATE_LCD, false); +} +#endif + /*** LCD controller - low level functions ***/ @@ -381,6 +410,9 @@ void lcd_update_rect(int, int, int, int) ICODE_ATTR; void lcd_update_rect(int x, int y, int width, int height) { int pixels = width * height; +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) + int original_height = height; +#endif fb_data* p = FBADDR(x,y); uint16_t* out = lcd_dblbuf[0]; @@ -411,6 +443,10 @@ void lcd_update_rect(int x, int y, int width, int height) } while (--height); } +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) + videoout_mirror_rgb565(lcd_dblbuf[0], x, y, width, + original_height, width); +#endif displaylcd_dma(pixels); } mutex_unlock(&lcd_mutex); @@ -435,8 +471,14 @@ void lcd_blit_yuv(unsigned char * const src[3], width = (width + 1) & ~1; /* ensure width is even */ +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) + int original_height = height; +#endif int pixels = width * height; uint16_t* out = lcd_dblbuf[0]; +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) + bool videoout_yuv_mirrored = false; +#endif z = stride * src_y; yuv_src[0] = src[0] + z + src_x; @@ -449,6 +491,14 @@ void lcd_blit_yuv(unsigned char * const src[3], { displaylcd_wait_dma(); +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) + /* MPEG playback already supplies planar YUV420. Send those planes + * directly to the qualified VP path instead of converting twice. */ + videoout_yuv_mirrored = videoout_mirror_yuv420( + yuv_src[0], yuv_src[1], yuv_src[2], 0, 0, stride, + x, y, width, height); +#endif + displaylcd_setup(x, y, width, height); height >>= 1; @@ -461,6 +511,12 @@ void lcd_blit_yuv(unsigned char * const src[3], out += width << 1; } while (--height); +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) + if (!videoout_yuv_mirrored) + videoout_mirror_rgb565(lcd_dblbuf[0], x, y, width, + original_height, width); +#endif + displaylcd_dma(pixels); } mutex_unlock(&lcd_mutex); @@ -525,6 +581,10 @@ void lcd_shutdown(void) #ifdef HAVE_LCD_SLEEP void lcd_sleep(void) { +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) + if (videoout_lcd_clock_required()) + return; +#endif lcd_powersave(); } diff --git a/firmware/target/arm/s5l8702/lcd-s5l8702.h b/firmware/target/arm/s5l8702/lcd-s5l8702.h index 8bed192a88..76c2a179ed 100644 --- a/firmware/target/arm/s5l8702/lcd-s5l8702.h +++ b/firmware/target/arm/s5l8702/lcd-s5l8702.h @@ -81,6 +81,11 @@ struct lcd_info_rec { void lcd_awake(void); +#if defined(HAVE_COMPOSITE_VIDEO_OUT) && !defined(BOOTLOADER) +void lcd_videoout_clock_acquire(void); +void lcd_videoout_clock_release(void); +#endif + #ifdef S5L_LCD_WITH_READID void lcd_read_display_id(int mupiface, uint8_t *lcd_id); #endif From 27332d64ad0212e084fb7783c90b8ae2f4f9410b Mon Sep 17 00:00:00 2001 From: David Cormier Date: Thu, 27 Aug 2026 23:22:51 -0300 Subject: [PATCH 153/408] ipod6g: add composite output setting Add Off, Auto, and On choices under the LCD settings menu for targets with HAVE_COMPOSITE_VIDEO_OUT. Off is the default and leaves accessory ADC, video registers, framebuffer conversion, output clocks, and CPU boost untouched. Auto enables output only for the qualified Philips dock signature. On still requires a physically present, fully identified dock and permits other detected composite accessories. Document the menu behavior and the corresponding configuration-file values in the iPod Classic manual. Hardware tested on an iPod Classic 6G/7G with a Philips DCP750/37. Change-Id: I649149daa331a95aa31110e1f2260a4a00ba789a --- apps/lang/english.lang | 14 ++++++++++++++ apps/menus/display_menu.c | 7 +++++++ apps/settings.c | 19 +++++++++++++++++++ apps/settings.h | 6 ++++++ apps/settings_list.c | 9 +++++++++ manual/appendix/config_file_options.tex | 3 +++ manual/configure_rockbox/display_options.tex | 7 +++++++ manual/platform/ipod6g.tex | 1 + 8 files changed, 66 insertions(+) diff --git a/apps/lang/english.lang b/apps/lang/english.lang index a4092a606c..078e08e3f9 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -17197,3 +17197,17 @@ *: "Filter Type" + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: "Composite Video Output" + + + *: "Composite Video Output" + + + *: "Composite Video Output" + + diff --git a/apps/menus/display_menu.c b/apps/menus/display_menu.c index f7b3efa2c4..0472b1c263 100644 --- a/apps/menus/display_menu.c +++ b/apps/menus/display_menu.c @@ -180,6 +180,10 @@ MENUITEM_SETTING(lcd_sleep_after_backlight_off, #ifdef HAVE_BACKLIGHT_BRIGHTNESS MENUITEM_SETTING(brightness_item, &global_settings.brightness, NULL); #endif +#ifdef HAVE_COMPOSITE_VIDEO_OUT +MENUITEM_SETTING(composite_video_output, + &global_settings.composite_video_output, NULL); +#endif #endif /* HAVE_BACKLIGHT */ #ifdef HAVE_LCD_CONTRAST MENUITEM_SETTING(contrast, &global_settings.contrast, NULL); @@ -214,6 +218,9 @@ MAKE_MENU(lcd_settings,ID2P(LANG_LCD_MENU), ,&brightness_item # endif #endif /* HAVE_BACKLIGHT */ +#ifdef HAVE_COMPOSITE_VIDEO_OUT + ,&composite_video_output +#endif #ifdef HAVE_LCD_CONTRAST ,&contrast #endif diff --git a/apps/settings.c b/apps/settings.c index 0423eebd29..f9b170a52d 100644 --- a/apps/settings.c +++ b/apps/settings.c @@ -34,6 +34,9 @@ #include "crc32.h" #include "sound.h" #include "settings.h" +#ifdef HAVE_COMPOSITE_VIDEO_OUT +#include "videoout.h" +#endif #include "debug.h" #include "usb.h" #include "backlight.h" @@ -873,6 +876,18 @@ void sound_settings_apply(void) #endif } +#ifdef HAVE_COMPOSITE_VIDEO_OUT +void settings_apply_videoout(int mode) +{ + if (mode < VIDEOOUT_OFF || mode > VIDEOOUT_ON) + mode = VIDEOOUT_OFF; + + global_settings.composite_video_output = mode; + videoout_set_mode((enum videoout_mode)mode, FBADDR(0, 0), + LCD_WIDTH, LCD_HEIGHT); +} +#endif + void settings_apply(bool read_disk) { logf("%s", __func__); @@ -1101,6 +1116,10 @@ void settings_apply(bool read_disk) #endif #endif /* HAVE_BACKLIGHT */ +#ifdef HAVE_COMPOSITE_VIDEO_OUT + settings_apply_videoout(global_settings.composite_video_output); +#endif + #ifndef HAS_BUTTON_HOLD set_selective_softlock_actions( global_settings.bt_selective_softlock_actions, diff --git a/apps/settings.h b/apps/settings.h index f5d036f9da..f76a0d9c3b 100644 --- a/apps/settings.h +++ b/apps/settings.h @@ -291,6 +291,9 @@ void settings_apply_skins(void); void settings_apply(bool read_disk); void settings_apply_pm_range(void); +#ifdef HAVE_COMPOSITE_VIDEO_OUT +void settings_apply_videoout(int mode); +#endif void settings_display(void); enum optiontype { RB_INT, RB_BOOL }; @@ -748,6 +751,9 @@ struct user_settings #ifdef HAVE_BACKLIGHT_BRIGHTNESS int brightness; #endif +#ifdef HAVE_COMPOSITE_VIDEO_OUT + int composite_video_output; +#endif #ifdef HAVE_REMOTE_LCD /* remote lcd */ diff --git a/apps/settings_list.c b/apps/settings_list.c index 7731d37e2c..26b67d8a74 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -32,6 +32,9 @@ #include "sound.h" #include "pcm_sink.h" #include "settings.h" +#ifdef HAVE_COMPOSITE_VIDEO_OUT +#include "videoout.h" +#endif #include "rbpaths.h" #include "settings_list.h" #include "usb.h" @@ -1303,6 +1306,12 @@ const struct settings_list settings[] = { #ifdef HAVE_LCD_FLIP OFFON_SETTING(0, flip_display, LANG_FLIP_DISPLAY, false, "flip display", NULL), +#endif +#ifdef HAVE_COMPOSITE_VIDEO_OUT + CHOICE_SETTING(0, composite_video_output, LANG_COMPOSITE_VIDEO_OUTPUT, + VIDEOOUT_OFF, "composite video output", "off,auto,on", + settings_apply_videoout, 3, + ID2P(LANG_OFF), ID2P(LANG_AUTO), ID2P(LANG_ON)), #endif /* display */ CHOICE_SETTING(F_TEMPVAR|F_THEMESETTING, cursor_style, LANG_INVERT_CURSOR, diff --git a/manual/appendix/config_file_options.tex b/manual/appendix/config_file_options.tex index d7adb64be9..b8e0e1f2e7 100644 --- a/manual/appendix/config_file_options.tex +++ b/manual/appendix/config_file_options.tex @@ -85,6 +85,9 @@ backlight on button hold & normal, off, on & N/A\\ caption backlight & on, off & N/A\\ brightness & \fixme{devise a way to get ranges from config-*.h} & N/A\\ + \opt{compositevideoout}{ + composite video output & off, auto, on & N/A\\ + } disk spindown & 3 to 254 & s\\ battery capacity & \fixme{devise a way to get ranges from config-*.h} & mAh\\ \opt{HAVE_CAR_ADAPTER_MODE}{ diff --git a/manual/configure_rockbox/display_options.tex b/manual/configure_rockbox/display_options.tex index f8b1344961..45d0b5b7ae 100755 --- a/manual/configure_rockbox/display_options.tex +++ b/manual/configure_rockbox/display_options.tex @@ -98,6 +98,13 @@ \item[Brightness.] Changes the brightness of your LCD display. } + \opt{compositevideoout}{ + \item[Composite Video Output.] + Mirrors the LCD and video playback through a supported composite + dock. \setting{Off} disables the output. \setting{Auto} enables it + only when a recognized video dock is connected. \setting{On} also + enables it for other detected dock accessories. + } } % \opt{HAVE_BACKLIGHT} \opt{lcd_contrast}{ diff --git a/manual/platform/ipod6g.tex b/manual/platform/ipod6g.tex index 96324c253a..fc77e03a25 100644 --- a/manual/platform/ipod6g.tex +++ b/manual/platform/ipod6g.tex @@ -4,6 +4,7 @@ \edef\UseOption{\UseOption,HAVE_BACKLIGHT} \edef\UseOption{\UseOption,HAVE_DISK_STORAGE} \edef\UseOption{\UseOption,HAVE_CAR_ADAPTER_MODE} +\edef\UseOption{\UseOption,HAVE_COMPOSITE_VIDEO_OUT} \edef\UseOption{\UseOption,ipod} \newcommand{\playerman}{Ipod} From 376db9bf5bb0d36466d23d572b72194292bb84f0 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Tue, 12 Jul 2022 22:57:29 +0100 Subject: [PATCH 154/408] touchscreen: Add flick gesture support in list and WPS Add some hardcoded flick gestures in the list UI: Top -> open quickscreen Left -> go back Right -> go to the WPS The WPS also gets a set of hardcoded flick shortcuts: Top -> open quickscreen Left -> go to file / database browser Right -> view playlist Bottom -> context menu These are enabled by default in pointing mode but can be disabled using the "Touchscreen Flick Shortcuts" option. Change-Id: Ib30a338ebb9662cd136985da6d34ed5b041e4077 --- apps/gui/bitmap/list.c | 26 ++++++++++++++++++++ apps/gui/wps.c | 20 +++++++++++++++ apps/lang/english.lang | 14 +++++++++++ apps/menus/display_menu.c | 10 ++++++-- apps/settings.h | 1 + apps/settings_list.c | 3 +++ manual/configure_rockbox/display_options.tex | 4 +++ 7 files changed, 76 insertions(+), 2 deletions(-) diff --git a/apps/gui/bitmap/list.c b/apps/gui/bitmap/list.c index ba155c0a16..c2e8896a3e 100644 --- a/apps/gui/bitmap/list.c +++ b/apps/gui/bitmap/list.c @@ -799,6 +799,25 @@ static int get_click_location(struct gui_synclist *list, int x, int y) return retval; } + +static int list_do_flick(const struct gesture_event *gevent) +{ + if (!global_settings.touch_enable_flick_shortcuts) + return ACTION_NONE; + + switch (gesture_flick_get(gevent)) + { + case GESTURE_FLICK_TOP: + return ACTION_STD_QUICKSCREEN; + case GESTURE_FLICK_LEFT: + return ACTION_STD_CANCEL; + case GESTURE_FLICK_RIGHT: + return ACTION_TREE_WPS; + default: + return ACTION_NONE; + } +} + unsigned gui_synclist_do_touchscreen(struct gui_synclist *list) { struct touchevent tevent; @@ -898,6 +917,13 @@ unsigned gui_synclist_do_touchscreen(struct gui_synclist *list) case GESTURE_DRAG: gesture_vel_process(&list_gvel, &tevent); + action = list_do_flick(&gevent); + if (action != ACTION_NONE) + { + action_gesture_reset(); + break; + } + if (list->scroll_mode == SCROLL_NONE) { list->scroll_base_y = list->y_pos; diff --git a/apps/gui/wps.c b/apps/gui/wps.c index 3a9ae8a6f4..12fda68824 100644 --- a/apps/gui/wps.c +++ b/apps/gui/wps.c @@ -170,6 +170,26 @@ static int skintouch_to_wps(void) { int offset = 0; struct gui_wps *gwps = skin_get_gwps(WPS, SCREEN_MAIN); + + struct gesture_event gevent; + if (global_settings.touch_enable_flick_shortcuts && + action_gesture_get_event(&gevent)) + { + switch (gesture_flick_get(&gevent)) + { + case GESTURE_FLICK_TOP: + return ACTION_WPS_QUICKSCREEN; + case GESTURE_FLICK_LEFT: + return ACTION_WPS_BROWSE; + case GESTURE_FLICK_RIGHT: + return ACTION_WPS_VIEW_PLAYLIST; + case GESTURE_FLICK_BOTTOM: + return ACTION_WPS_CONTEXT; + default: + break; + } + } + int button = skin_get_touchaction(gwps, &offset); switch (button) { diff --git a/apps/lang/english.lang b/apps/lang/english.lang index 078e08e3f9..f1d4305cc6 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -17211,3 +17211,17 @@ *: "Composite Video Output" + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: "Touchscreen Flick Shortcuts" + + + *: "Touchscreen Flick Shortcuts" + + + *: "Touchscreen Flick Shortcuts" + + diff --git a/apps/menus/display_menu.c b/apps/menus/display_menu.c index 0472b1c263..99b1ad5f1e 100644 --- a/apps/menus/display_menu.c +++ b/apps/menus/display_menu.c @@ -580,6 +580,8 @@ static int line_padding_callback(int action, } MENUITEM_SETTING(touch_mode, &global_settings.touch_mode, touch_mode_callback); +MENUITEM_SETTING(touch_enable_flick_shortcuts, + &global_settings.touch_enable_flick_shortcuts, NULL); MENUITEM_FUNCTION(touchscreen_menu_calibrate, 0, ID2P(LANG_TOUCHSCREEN_CALIBRATE), calibrate, NULL, Icon_NOICON); @@ -588,8 +590,12 @@ MENUITEM_FUNCTION(touchscreen_menu_reset_calibration, 0, reset_mapping, NULL, Icon_NOICON); MENUITEM_SETTING(list_line_padding, &global_settings.list_line_padding, line_padding_callback); -MAKE_MENU(touchscreen_menu, ID2P(LANG_TOUCHSCREEN_SETTINGS), NULL, Icon_NOICON, &list_line_padding, &touch_mode, - &touchscreen_menu_calibrate, &touchscreen_menu_reset_calibration); +MAKE_MENU(touchscreen_menu, ID2P(LANG_TOUCHSCREEN_SETTINGS), NULL, Icon_NOICON, + &touch_mode, + &touch_enable_flick_shortcuts, + &list_line_padding, + &touchscreen_menu_calibrate, + &touchscreen_menu_reset_calibration); #endif static int codepage_callback(int action, diff --git a/apps/settings.h b/apps/settings.h index f76a0d9c3b..5a0f4f83e3 100644 --- a/apps/settings.h +++ b/apps/settings.h @@ -805,6 +805,7 @@ struct user_settings #ifdef HAVE_TOUCHSCREEN int touch_mode; + bool touch_enable_flick_shortcuts; struct touchscreen_parameter ts_calibration_data; struct list_kinetic_scroll_settings kinetic_scroll_accel; struct list_kinetic_scroll_settings kinetic_scroll_brake; diff --git a/apps/settings_list.c b/apps/settings_list.c index 26b67d8a74..24edac9bb7 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -2432,6 +2432,9 @@ const struct settings_list settings[] = { CHOICE_SETTING(0, touch_mode, LANG_TOUCHSCREEN_MODE, DEFAULT_TOUCHSCREEN_MODE, "touchscreen mode", "point,grid", NULL, 2, ID2P(LANG_TOUCHSCREEN_POINT), ID2P(LANG_TOUCHSCREEN_GRID)), + CHOICE_SETTING(0, touch_enable_flick_shortcuts, LANG_TOUCHSCREEN_FLICK_SHORTCUTS, true, + "touchscreen flick shortcuts", "off,on", NULL, + 2, ID2P(LANG_OFF), ID2P(LANG_ON)), CUSTOM_SETTING(0, ts_calibration_data, -1, &default_calibration_parameters, "touchscreen calibration", tsc_load_from_cfg, tsc_write_to_cfg, diff --git a/manual/configure_rockbox/display_options.tex b/manual/configure_rockbox/display_options.tex index 45d0b5b7ae..187ed36ba8 100755 --- a/manual/configure_rockbox/display_options.tex +++ b/manual/configure_rockbox/display_options.tex @@ -310,6 +310,10 @@ particular, \setting{3x3 Grid} mode will be used in the WPS unless the theme supports touch interaction; the default ``Cabbie v2.0'' theme does not include touch support.} + \item[Touchscreen Flick Shortcuts.] + Enables or disables flick shortcuts in \setting{Absolute Point} mode. + A ``flick'' is any quick swiping motion from the top, bottom, left, or + right edges of the screen towards the center. \item[Line Padding In Lists.] This option affects the amount of padding added to list entries, which makes them larger on screen and easier to select with a touchscreen. From cbd8b68e06d84a0405e1233428130962931afdb4 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Mon, 7 Sep 2026 09:00:07 -0400 Subject: [PATCH 155/408] manual: Remove executable bit off of files that shouldn't have it Change-Id: I1524a574d44f0f1bc5cb121fab949a82ca8cc802 --- manual/configure_rockbox/display_options.tex | 0 manual/plugins/images/ss-xrick-220x176x16.png | Bin manual/plugins/images/ss-xrick-240x320x16.png | Bin manual/plugins/images/ss-xrick-320x240x16.png | Bin 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 manual/configure_rockbox/display_options.tex mode change 100755 => 100644 manual/plugins/images/ss-xrick-220x176x16.png mode change 100755 => 100644 manual/plugins/images/ss-xrick-240x320x16.png mode change 100755 => 100644 manual/plugins/images/ss-xrick-320x240x16.png diff --git a/manual/configure_rockbox/display_options.tex b/manual/configure_rockbox/display_options.tex old mode 100755 new mode 100644 diff --git a/manual/plugins/images/ss-xrick-220x176x16.png b/manual/plugins/images/ss-xrick-220x176x16.png old mode 100755 new mode 100644 diff --git a/manual/plugins/images/ss-xrick-240x320x16.png b/manual/plugins/images/ss-xrick-240x320x16.png old mode 100755 new mode 100644 diff --git a/manual/plugins/images/ss-xrick-320x240x16.png b/manual/plugins/images/ss-xrick-320x240x16.png old mode 100755 new mode 100644 From 064c16536702497b07c67d06f277f566a5029432 Mon Sep 17 00:00:00 2001 From: David Cormier Date: Sat, 5 Sep 2026 11:16:14 -0300 Subject: [PATCH 156/408] libm4a: handle sparse chunk maps and video-first MP4 Size the reduced stco lookup with ceiling division. The table stores chunk entries 0, divider, 2 * divider, and so on, so floor division allocated one entry too few whenever the original count had a remainder. Treat a valid media-information atom whose first child is not smhd as a non-audio track and skip the remainder. Continue scanning subsequent tracks so AAC decoding works when an MP4 places its video track before the audio track, while still rejecting malformed atom sizes and malformed sound headers. Keep these container fixes independent of the H.264 player and target driver so they can be reviewed and applied to libm4a on their own. Build-tested as part of the normal and isolated-runtime iPod 6G configurations and hardware-tested with AAC audio in M4V playback. Change-Id: I8c711525932f54ecbdad982c7f7ddc9490c5668d --- lib/rbcodec/codecs/libm4a/demux.c | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/rbcodec/codecs/libm4a/demux.c b/lib/rbcodec/codecs/libm4a/demux.c index f9203a0893..869f23535f 100644 --- a/lib/rbcodec/codecs/libm4a/demux.c +++ b/lib/rbcodec/codecs/libm4a/demux.c @@ -478,7 +478,11 @@ static bool read_chunk_stco(qtmovie_t *qtmovie, size_t chunk_len) else { // we failed to alloc memory for lookup table, so reduce seek accuracy and try again - fit_numentries = numentries / accuracy_divider; + /* Entries 0, divider, 2*divider, ... require a ceiling divide. + * The old floor allocation overran by one entry whenever the + * chunk count was not evenly divisible. */ + fit_numentries = + (numentries + accuracy_divider - 1) / accuracy_divider; } } DEBUGF("lookup_table numentries %d, fit_numentries %ld\n", numentries, fit_numentries); @@ -638,18 +642,26 @@ static bool read_chunk_minf(qtmovie_t *qtmovie, size_t chunk_len) size_t size_remaining = chunk_len - 8; uint32_t i; - /* Check for smhd, only kind of minf we care about */ - - if ((i = stream_read_uint32(qtmovie->stream)) != 16) + /* Check for smhd, the only kind of minf we care about. MP4 video files + * normally put a video trak before the AAC trak. Older Rockbox code + * treated that vmhd as fatal, which made aac.codec unable to decode the + * audio from an otherwise ordinary .mp4/.m4v. Skip a non-audio minf and + * continue looking for the sound track. */ + i = stream_read_uint32(qtmovie->stream); + if (i < 8 || i > size_remaining) { DEBUGF("unexpected size in media info: %ld\n", (long)i); - stream_skip(qtmovie->stream, size_remaining-4); - return true; + return false; } if (stream_read_uint32(qtmovie->stream) != MAKEFOURCC('s','m','h','d')) { - DEBUGF("not a sound header! can't handle this.\n"); + stream_skip(qtmovie->stream, size_remaining - 8); + return true; + } + if (i != 16) + { + DEBUGF("unexpected sound header size: %ld\n", (long)i); return false; } From 76a2fdfc1f9008856b64909dbce93df8fa3b0a94 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Mon, 7 Sep 2026 09:24:56 -0400 Subject: [PATCH 157/408] lang: Restrict LANG_COMPOSITE_VIDEO_OUTPUT to devices that have it Currently just ipod6g, but treat it as a generic feature Change-Id: Idda31455765908e4bd099cf5117b858bb3848f7f --- apps/features.txt | 4 ++++ apps/lang/english.lang | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/features.txt b/apps/features.txt index 0c4719ca11..68351afee3 100644 --- a/apps/features.txt +++ b/apps/features.txt @@ -324,3 +324,7 @@ usbdac #if defined(UTF8PROC_EXPORTS) utf8proc #endif + +#if defined(HAVE_COMPOSITE_VIDEO_OUT) +videoout +#endif diff --git a/apps/lang/english.lang b/apps/lang/english.lang index f1d4305cc6..3a7a2115da 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -17202,13 +17202,16 @@ desc: Enables composite video framebuffer mirroring on supported targets user: core - *: "Composite Video Output" + *: none + videoout: "Composite Video Output" - *: "Composite Video Output" + *: none + videoout: "Composite Video Output" - *: "Composite Video Output" + *: none + videoout: "Composite Video Output" From 4f54dbec79a90cb07d9a4f628ffba101c7fc4b81 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Mon, 7 Sep 2026 09:52:40 -0400 Subject: [PATCH 158/408] lang: restrict LANG_TOUCHSCREEN_FLICK_SHORTCUTS to touchscreen devices Change-Id: Iaabc39901eadca6d36d663f7feca52273041afac --- apps/lang/english.lang | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/lang/english.lang b/apps/lang/english.lang index 3a7a2115da..4c2f0d58b0 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -17219,12 +17219,15 @@ desc: in settings menu user: core - *: "Touchscreen Flick Shortcuts" + *: none + touchscreen: "Touchscreen Flick Shortcuts" - *: "Touchscreen Flick Shortcuts" + *: none + touchscreen: "Touchscreen Flick Shortcuts" - *: "Touchscreen Flick Shortcuts" + *: none + touchscreen: "Touchscreen Flick Shortcuts" From a70f30adf165644521b91b06f7eb2d08858713c1 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Mon, 7 Sep 2026 18:02:55 +0300 Subject: [PATCH 159/408] Bulgarian translation update Change-Id: I96bc87fb873e9eb4a6ca96283873459314e0992f --- apps/lang/bulgarian.lang | 120 +++++++++------------------------------ 1 file changed, 28 insertions(+), 92 deletions(-) diff --git a/apps/lang/bulgarian.lang b/apps/lang/bulgarian.lang index f0d165cceb..5f711a6eb9 100644 --- a/apps/lang/bulgarian.lang +++ b/apps/lang/bulgarian.lang @@ -1009,10 +1009,10 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Пиков филтър %d" + *: "Пиков филтър" *: "Пиков филтър" @@ -17102,128 +17102,64 @@ - id: LANG_SET_CONTEXT_ITEM - desc: WPS context item + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu user: core - *: "Set Context Item" + *: "Filter %d" - *: "Избери елемент за контекст" + *: "Филтър %d" - *: "Избери елемент за контекст" + *: "Филтър" - id: LANG_FILE_NOT_FOUND - desc: When file does not exist + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu user: core - *: "File not found" + *: "Filter Type" - *: "Не е намерен файл" + *: "Вид на филтъра" - *: "Не е намерен файл" + *: "Вид на филтъра" - id: LANG_SHOW_IN_FILES - desc: Reveal item in File Browser + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets user: core - *: "Show in Files" + *: none + videoout: "Composite Video Output" - *: "Покажи във файлов мениджър" + *: none + videoout: "Композитен видео изход" - *: "Покажи във файлов мениджър" + *: none + videoout: "Композитен видео изход" - id: LANG_CHANNEL_SWAP - desc: in sound_settings + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu user: core - *: "Swap Left & Right" + *: none + touchscreen: "Touchscreen Flick Shortcuts" - *: "Размяна на ляво/дясно" + *: none + touchscreen: "Преки пътища на тъчскрийн" - *: "Размяна на ляво и дясно" - - - - id: LANG_COUNTDOWN_TIMER_SET - desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration - user: core - - *: "SET TIMER" - - - *: "НАСТРОЙКА ТАЙМЕР" - - - *: "Настройка таймер" - - - - id: LANG_COUNTDOWN_TIMER_RUNNING - desc: countdown_timer plugin - status label shown while the countdown is active - user: core - - *: "RUNNING" - - - *: "АКТИВЕН" - - - *: "Активен" - - - - id: LANG_COUNTDOWN_TIMER_PAUSED - desc: countdown_timer plugin - status label shown while the countdown is paused - user: core - - *: "PAUSED" - - - *: "ПАУЗА" - - - *: "Пауза" - - - - id: LANG_COUNTDOWN_TIMER_OVERTIME - desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up - user: core - - *: "OVERTIME" - - - *: "ИЗВЪНРЕДНО" - - - *: "Извънредно" - - - - id: LANG_COUNTDOWN_TIMER_FINISHED - desc: countdown_timer plugin - status label shown at the moment the countdown expires - user: core - - *: "FINISHED" - - - *: "ЗАВЪРШЕН" - - - *: "Завършен" + *: none + touchscreen: "Преки пътища на тъчскрийн" From 54b26ac9f15c66c674774b4b4bd1087c2b9acfe1 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Mon, 7 Sep 2026 12:34:38 -0400 Subject: [PATCH 160/408] Translation updates: - Chinese Simplified (Wang Ji) - Polish (Adam Rak) - Slovak (Matej Golian) - US English (Myself) Change-Id: I939907d653309eabdb8d8c722bc953749a818898 --- apps/lang/chinese-simp.lang | 34 ++++++++++++++++++++++++++++++++++ apps/lang/english-us.lang | 34 ++++++++++++++++++++++++++++++++++ apps/lang/polski.lang | 34 ++++++++++++++++++++++++++++++++++ apps/lang/slovak.lang | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) diff --git a/apps/lang/chinese-simp.lang b/apps/lang/chinese-simp.lang index 22d72b5af0..3c8be16c72 100644 --- a/apps/lang/chinese-simp.lang +++ b/apps/lang/chinese-simp.lang @@ -17134,3 +17134,37 @@ *: "滤波器类型" + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "复合视频(AV)输出" + + + *: none + videoout: "复合视频输出" + + + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "触控滑动快捷方式" + + + *: none + touchscreen: "触控滑动快捷方式" + + diff --git a/apps/lang/english-us.lang b/apps/lang/english-us.lang index b5345c0f33..55028062cd 100644 --- a/apps/lang/english-us.lang +++ b/apps/lang/english-us.lang @@ -17124,3 +17124,37 @@ *: "Filter Type" + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "Composite Video Output" + + + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + diff --git a/apps/lang/polski.lang b/apps/lang/polski.lang index de32b4f905..19320cbe5c 100644 --- a/apps/lang/polski.lang +++ b/apps/lang/polski.lang @@ -17134,3 +17134,37 @@ *: "Typ filtra" + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "Wyjście wideo kompozytowe" + + + *: none + videoout: "Wyjście wideo kompozytowe" + + + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "Skróty ekranu dotykowego" + + + *: none + touchscreen: "Skróty ekranu dotykowego" + + diff --git a/apps/lang/slovak.lang b/apps/lang/slovak.lang index 76af43a20b..7f459d8ba5 100644 --- a/apps/lang/slovak.lang +++ b/apps/lang/slovak.lang @@ -17127,3 +17127,37 @@ *: "Typ filtra" + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "Kompozitný video výstup" + + + *: none + videoout: "Kompozitný video výstup" + + + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "Skratky švihnutia na obrazovke" + + + *: none + touchscreen: "Skratky švihnutia na obrazovke" + + From a967c5a01881cf79a3c5a26ac16a123638a9742f Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Mon, 7 Sep 2026 22:21:49 -0400 Subject: [PATCH 161/408] Translation updates - German (Wilfried Winkler) - Italian (Alessio Lenzi) Change-Id: I52d6751364eae722743a5fdb1ece51c626498677 --- apps/lang/deutsch.lang | 68 +++++++++++++++++++++++++++++++++++++++-- apps/lang/italiano.lang | 34 +++++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/apps/lang/deutsch.lang b/apps/lang/deutsch.lang index 5ba440c881..6f3db1d509 100644 --- a/apps/lang/deutsch.lang +++ b/apps/lang/deutsch.lang @@ -1010,13 +1010,13 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Bandfilter %d" + *: "Glockenfilter" - *: "Bandfilter" + *: "Glockenfilter" @@ -17118,3 +17118,65 @@ *: "Kontextelement festlegen" + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Filter %d" + + + *: "Filter" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Filter Typ" + + + *: "Filter Typ" + + + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "Composite Video Ausgang" + + + *: none + videoout: "Composite Video Ausgang" + + + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "Wischgesten Kurzbefehle" + + + *: none + touchscreen: "Wischgesten Kurzbefehle" + + diff --git a/apps/lang/italiano.lang b/apps/lang/italiano.lang index 2efb87b88f..8dd4d21165 100644 --- a/apps/lang/italiano.lang +++ b/apps/lang/italiano.lang @@ -17127,3 +17127,37 @@ *: "Tipo Filtro" + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "Uscita Video Composito" + + + *: none + videoout: "Uscita Video Composito" + + + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "Scorciatoie mediante lo scorrimento sul touchscreen" + + + *: none + touchscreen: "Scorciatoie mediante lo scorrimento sul touchscreen" + + From 10ec9bd530c852631ce126795cacf20e7578017f Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Mon, 7 Sep 2026 12:00:29 +0200 Subject: [PATCH 162/408] quickscreen: eliminate excess/pad variable from vp setup Simplifies commit da207d606 a bit more Change-Id: I7ef98914153c0c0bfb5a3ff594fcb7e98f2dc5ba --- apps/gui/quickscreen.c | 81 +++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/apps/gui/quickscreen.c b/apps/gui/quickscreen.c index 3cf4a58a75..cfd70ebe8e 100644 --- a/apps/gui/quickscreen.c +++ b/apps/gui/quickscreen.c @@ -67,14 +67,13 @@ void quickscreen_set_skinned(enum screen_type screen, bool skinned) } /* Set up icons viewport */ -static inline void setup_icons(struct viewport *vp_icons, int excess_width, - int top_y, int top_height, int bottom_y) +static inline void setup_icons(struct viewport *vp_icons, struct viewport *vps) { - vp_icons->x += (vp_icons->width - CENTER_ICONAREA_SIZE - excess_width)/2; - vp_icons->y = top_y + top_height; + vp_icons->x = vps[QUICKSCREEN_LEFT].x + vps[QUICKSCREEN_LEFT].width; + vp_icons->y = vps[QUICKSCREEN_TOP].y + vps[QUICKSCREEN_TOP].height; - vp_icons->width = CENTER_ICONAREA_SIZE + excess_width; - vp_icons->height = bottom_y - vp_icons->y; + vp_icons->width = vps[QUICKSCREEN_RIGHT].x - vp_icons->x; + vp_icons->height = vps[QUICKSCREEN_BOTTOM].y - vp_icons->y; /* Shrink icons vp by a few pixels if there is room, so the arrows aren't drawn right next to the text */ @@ -108,7 +107,7 @@ static inline void set_y_axis(struct viewport *left, struct viewport *right, if (nb_lines >= MAX_NEEDED_LINES) /* more than enough vertical space */ { - top->y += MARGIN; + top->y += MARGIN; bottom->y -= MARGIN; } if (nb_lines >= 2) /* Single line uses parent height and y position */ @@ -119,15 +118,14 @@ static inline void set_y_axis(struct viewport *left, struct viewport *right, } } -/* Set x-position and width for left and right text viewport. - Returns excess horizontal space, after accounting for icon area size. */ -static inline int set_x_axis(struct viewport *left, struct viewport *right, - int width) +/* Set x-position and width for left and right text viewport */ +static inline void set_x_axis(struct viewport *left, struct viewport *right, + int width) { - int pad = 0, excess = 0; int parent_width = left->width; + int remaining_width = parent_width - width*2 - CENTER_ICONAREA_SIZE; - if (width*2 + CENTER_ICONAREA_SIZE > parent_width) + if (remaining_width < 0) { /* Shrink left and right text vps */ width = parent_width; @@ -139,21 +137,14 @@ static inline int set_x_axis(struct viewport *left, struct viewport *right, if (width >= 2) width /= 2; } - else + else if (remaining_width > MARGIN*4) { - /* space for padding or for excess gap between icons */ - excess = parent_width - CENTER_ICONAREA_SIZE - width*2; - if (excess > MARGIN*4) - { - pad = MARGIN; - excess -= MARGIN*2; - } + left->x += MARGIN; + right->x -= MARGIN; } - left->x += pad; - right->x += parent_width - width - pad; - right->width = left->width = width; - return excess; + right->x += parent_width - width; + right->width = left->width = width; } /* Set up all QS viewports */ @@ -162,43 +153,35 @@ static void quickscreen_setup_viewports(struct quickscreen *qs, { struct viewport *parent = &qs->parent[screen]; struct viewport *vps = qs->vps[screen]; - int width = 0, excess; - const char *str; + int width = 0; qs->vp_icons[screen] = *parent; FOR_QS_ITEMS(i) + { vps[i] = *parent; + vps[i].flags &= ~VP_FLAG_ALIGNMENT_MASK; - /* Calculate left and right item's maximum title width */ - if (qs->items[QUICKSCREEN_LEFT]) - { - str = P2STR(ID2P(qs->items[QUICKSCREEN_LEFT]->lang_id)); - width = font_getstringsize(str, NULL, NULL, parent->font); - } - if (qs->items[QUICKSCREEN_RIGHT]) - { - str = P2STR(ID2P(qs->items[QUICKSCREEN_RIGHT]->lang_id)); + /* Check maximum width needed for left or right text vp */ + if (!qs->items[i] || (i != QUICKSCREEN_LEFT && i != QUICKSCREEN_RIGHT)) + continue; + const char *str = P2STR(ID2P(qs->items[i]->lang_id)); width = MAX(font_getstringsize(str, NULL, NULL, parent->font), width); } + /* Set text alignment */ + vps[QUICKSCREEN_RIGHT].flags |= VP_FLAG_ALIGN_RIGHT; + vps[QUICKSCREEN_TOP].flags |= VP_FLAG_ALIGN_CENTER; + vps[QUICKSCREEN_BOTTOM].flags |= VP_FLAG_ALIGN_CENTER; - /* Set x-position and width for left and right text viewports. Top and bottom - use entire width. Check for excess space after accounting for icon area */ - excess = set_x_axis(&vps[QUICKSCREEN_LEFT], &vps[QUICKSCREEN_RIGHT], width); + /* Set x-position and width for left and right text viewports + (top and bottom use parent's entire width) */ + set_x_axis(&vps[QUICKSCREEN_LEFT], &vps[QUICKSCREEN_RIGHT], width); /* Set y-position and height for all text viewports */ set_y_axis(&vps[QUICKSCREEN_LEFT], &vps[QUICKSCREEN_RIGHT], &vps[QUICKSCREEN_TOP], &vps[QUICKSCREEN_BOTTOM]); - /* Configure icons viewport based on text vp dimensions and excess width */ - setup_icons(&qs->vp_icons[screen], excess, vps[QUICKSCREEN_TOP].y, - vps[QUICKSCREEN_TOP].height, vps[QUICKSCREEN_BOTTOM].y); - - /* text alignment */ - vps[QUICKSCREEN_LEFT].flags &= ~VP_FLAG_ALIGNMENT_MASK; /* left-aligned */ - vps[QUICKSCREEN_TOP].flags |= VP_FLAG_ALIGN_CENTER; /* centered */ - vps[QUICKSCREEN_BOTTOM].flags |= VP_FLAG_ALIGN_CENTER; /* centered */ - vps[QUICKSCREEN_RIGHT].flags &= ~VP_FLAG_ALIGNMENT_MASK; /* right-aligned */ - vps[QUICKSCREEN_RIGHT].flags |= VP_FLAG_ALIGN_RIGHT; + /* Icons viewport fills center area */ + setup_icons(&qs->vp_icons[screen], vps); } /* Draw settings item into current viewport */ From f2985dc8a25d71be6ace308d25824bf104624d33 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Wed, 9 Sep 2026 11:22:57 +0200 Subject: [PATCH 163/408] option_get_valuestring: fix trailing whitespace for UNIT_INT Remove trailing whitespace from int settings, so that right-aligned or centered values look correct in Quickscreen for settings such as Brightness. Change-Id: I006cf3c3e8b30be2246e7370f26e0428fa47e54b --- apps/gui/option_select.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/gui/option_select.c b/apps/gui/option_select.c index 12dff945ad..d70af4d042 100644 --- a/apps/gui/option_select.c +++ b/apps/gui/option_select.c @@ -108,7 +108,12 @@ const char *option_get_valuestring(const struct settings_list *setting, if (formatter) str = formatter(buffer, buf_len, (int)temp_var, str_unit); else if (!is_time_setting) - snprintf(buffer, buf_len, "%d %s", (int)temp_var, str_unit?str_unit:""); + { + if (str_unit && str_unit[0]) + snprintf(buffer, buf_len, "%d %s", (int)temp_var, str_unit); + else + snprintf(buffer, buf_len, "%d", (int)temp_var); + } } else if (HASFLAG(setting, F_T_SOUND)) { From 7e3782868cfc685bc253774bc57ef310127ef742 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Wed, 9 Sep 2026 11:24:18 +0200 Subject: [PATCH 164/408] quickscreen: fix icon x-position off-by-one Top, bottom, and right icons were displayed too far to the left. Apparently a long-standing issue. Icon width is 7, so subtract 7/2 = 3 pixels, instead of 4, from calculated center, and only 7 pixels, instead of 8, from the right vp edge. Change-Id: I8440a4fef4778a66d56117a05b3fe4c36ac8dadd --- apps/gui/quickscreen.c | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/gui/quickscreen.c b/apps/gui/quickscreen.c index cfd70ebe8e..a04c4bb2cd 100644 --- a/apps/gui/quickscreen.c +++ b/apps/gui/quickscreen.c @@ -42,6 +42,8 @@ text and icons, or between text and parent boundaries. */ #define MARGIN 10 #define CENTER_ICONAREA_SIZE (MARGIN + 8*2) +#define QS_ICON_WIDTH 7 +#define QS_ICON_HEIGHT 8 #define FOR_QS_ITEMS(i) for (int i = 0; i < QUICKSCREEN_ITEM_COUNT; i++) @@ -249,24 +251,33 @@ static void quickscreen_draw(struct quickscreen *qs, enum screen_type screen) } /* icons */ - if (parent->width > CENTER_ICONAREA_SIZE && vp_icons->height >= 8) + if (parent->width > CENTER_ICONAREA_SIZE && vp_icons->height >= QS_ICON_HEIGHT) { display->set_viewport(vp_icons); - if (qs->items[QUICKSCREEN_TOP]) - display->mono_bitmap(bitmap_icons_7x8[Icon_UpArrow], - (vp_icons->width/2) - 4, 0, 7, 8); - - if (qs->items[QUICKSCREEN_RIGHT]) - display->mono_bitmap(bitmap_icons_7x8[Icon_FastForward], - vp_icons->width - 8, (vp_icons->height/2) - 4, 7, 8); if (qs->items[QUICKSCREEN_LEFT]) display->mono_bitmap(bitmap_icons_7x8[Icon_FastBackward], - 0, (vp_icons->height/2) - 4, 7, 8); + 0, + vp_icons->height/2 - QS_ICON_HEIGHT/2, + QS_ICON_WIDTH, QS_ICON_HEIGHT); + + if (qs->items[QUICKSCREEN_RIGHT]) + display->mono_bitmap(bitmap_icons_7x8[Icon_FastForward], + vp_icons->width - QS_ICON_WIDTH, + vp_icons->height/2 - QS_ICON_HEIGHT/2, + QS_ICON_WIDTH, QS_ICON_HEIGHT); + + if (qs->items[QUICKSCREEN_TOP]) + display->mono_bitmap(bitmap_icons_7x8[Icon_UpArrow], + vp_icons->width/2 - QS_ICON_WIDTH/2, + 0, + QS_ICON_WIDTH, QS_ICON_HEIGHT); if (qs->items[QUICKSCREEN_BOTTOM]) display->mono_bitmap(bitmap_icons_7x8[Icon_DownArrow], - (vp_icons->width/2) - 4, vp_icons->height - 8, 7, 8); + vp_icons->width/2 - QS_ICON_WIDTH/2, + vp_icons->height - QS_ICON_HEIGHT, + QS_ICON_WIDTH, QS_ICON_HEIGHT); } skin_mark_dirty(screen); From 57a91121f6a1a9d86931d41a2c52386e0a416d78 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Sat, 12 Sep 2026 07:56:10 +0200 Subject: [PATCH 165/408] root_menu: remove ACTIVITY_UNKNOWN hack for plugins This was added in commit 8ff2c81bd to prevent a custom root menu UI from flashing on screen in some themes when returning from a plugin. Because the display isn't updated in this scenario anymore, the workaround can be safely eliminated. Change-Id: Ie46b0921cfdad6e45a31661a5d451dc2717171d6 --- apps/root_menu.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/root_menu.c b/apps/root_menu.c index a546432d85..a989475740 100644 --- a/apps/root_menu.c +++ b/apps/root_menu.c @@ -1032,10 +1032,7 @@ void root_menu(void) } } - - push_activity_without_refresh(ACTIVITY_UNKNOWN); /* prevent plugin_load */ - next_screen = load_plugin_screen(key); /* from flashing root */ - pop_current_activity_without_refresh(); /* menu activity */ + next_screen = load_plugin_screen(key); if (next_screen == GO_TO_PREVIOUS) { From e0136bc7f4fc210b018fb95fc84e5d0b0ed5de72 Mon Sep 17 00:00:00 2001 From: David Cormier Date: Sat, 5 Sep 2026 11:34:23 -0300 Subject: [PATCH 166/408] ipod6g: add hardware H.264 video playback Add S5L8702 VPU-B initialization, reset, bitstream input, frame output, and cache maintenance to the iPod 6G target. Advertise it with HAVE_HW_H264 and append a capability-gated decoder interface to the plugin API. Keep MP4 parsing, AAC decode and mixer output, A/V synchronization, playback controls, resume state, and presentation in the multi-file h264_player plugin. The target layer exposes decoder operations only. Accept non-fragmented MP4/M4V containing Constrained Baseline H.264 through level 3.0, up to 640x480 at 30 fps, with optional AAC-LC audio. Validate codec configuration and all sample-table relationships before activating the hardware. Read MP4 tables in bulk, boost the CPU while preparing them, and report staged loading progress so long movies do not appear to hang during startup. Register the viewer and document its format limits and controls. The libm4a compatibility fixes needed by video-first containers remain in the preceding standalone change. Tested on an iPod Classic 6G through an isolated Rolo nightly runtime: H.264/AAC playback and M4V startup succeeded. Normal and isolated-runtime iPod 6G builds also complete, and git diff --check is clean. Change-Id: I1e96c65c7d0b4231a94f602f8052f1b26d6e4a80 --- apps/plugin.c | 6 + apps/plugin.h | 8 +- apps/plugins/SUBDIRS | 4 + apps/plugins/h264_player/SOURCES | 5 + apps/plugins/h264_player/h264_player.c | 51 + apps/plugins/h264_player/h264_player.make | 26 + apps/plugins/h264_player/mp4_demux.c | 1548 +++++++++++++++++ apps/plugins/h264_player/mp4_demux.h | 150 ++ apps/plugins/h264_player/video_audio.c | 563 ++++++ apps/plugins/h264_player/video_audio.h | 60 + apps/plugins/h264_player/video_pcm.c | 250 +++ apps/plugins/h264_player/video_pcm.h | 52 + apps/plugins/h264_player/video_playback.c | 1132 ++++++++++++ apps/plugins/h264_player/video_playback.h | 21 + apps/plugins/viewers.config | 5 + docs/CREDITS | 1 + firmware/SOURCES | 4 + firmware/export/config/ipod6g.h | 5 + firmware/export/hw_h264.h | 39 + firmware/export/s5l87xx.h | 22 + firmware/target/arm/s5l8702/ipod6g/vpu-6g.c | 71 + firmware/target/arm/s5l8702/ipod6g/vpu-6g.h | 48 + .../target/arm/s5l8702/ipod6g/vpu-h264-6g.c | 1081 ++++++++++++ .../target/arm/s5l8702/ipod6g/vpu-h264-6g.h | 84 + firmware/target/arm/s5l8702/system-s5l8702.c | 3 + manual/platform/ipod6g.tex | 1 + manual/plugins/h264_player.tex | 26 + manual/plugins/main.tex | 5 + 28 files changed, 5270 insertions(+), 1 deletion(-) create mode 100644 apps/plugins/h264_player/SOURCES create mode 100644 apps/plugins/h264_player/h264_player.c create mode 100644 apps/plugins/h264_player/h264_player.make create mode 100644 apps/plugins/h264_player/mp4_demux.c create mode 100644 apps/plugins/h264_player/mp4_demux.h create mode 100644 apps/plugins/h264_player/video_audio.c create mode 100644 apps/plugins/h264_player/video_audio.h create mode 100644 apps/plugins/h264_player/video_pcm.c create mode 100644 apps/plugins/h264_player/video_pcm.h create mode 100644 apps/plugins/h264_player/video_playback.c create mode 100644 apps/plugins/h264_player/video_playback.h create mode 100644 firmware/export/hw_h264.h create mode 100644 firmware/target/arm/s5l8702/ipod6g/vpu-6g.c create mode 100644 firmware/target/arm/s5l8702/ipod6g/vpu-6g.h create mode 100644 firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c create mode 100644 firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.h create mode 100644 manual/plugins/h264_player.tex diff --git a/apps/plugin.c b/apps/plugin.c index 51b4478421..2e747fca61 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -49,6 +49,9 @@ #include "language.h" #include "statusbar-skinned.h" #include "panic.h" +#ifdef HAVE_HW_H264 +#include "hw_h264.h" +#endif #if CONFIG_CHARGING #include "power.h" @@ -880,6 +883,9 @@ static const struct plugin_api rockbox_api = { gesture_flick_get_in_vp, gesture_flick_get, #endif +#ifdef HAVE_HW_H264 + &target_hw_h264_api, +#endif }; static int plugin_buffer_handle; diff --git a/apps/plugin.h b/apps/plugin.h index cc10371a35..46e32d8b9b 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -83,6 +83,9 @@ int plugin_open(const char *plugin, const char *parameter); #include "metadata.h" #include "sound.h" #include "audio.h" +#ifdef HAVE_HW_H264 +#include "hw_h264.h" +#endif #include "voice_thread.h" #include "root_menu.h" #include "talk.h" @@ -179,7 +182,7 @@ int plugin_open(const char *plugin, const char *parameter); * when this happens please take the opportunity to sort in * any new functions "waiting" at the end of the list. */ -#define PLUGIN_API_VERSION 284 +#define PLUGIN_API_VERSION 285 /* 239 Marks the removal of ARCHOS HWCODEC and CHARCELL */ @@ -1046,6 +1049,9 @@ struct plugin_api { const struct viewport *vp); int (*gesture_flick_get)(const struct gesture_event *gevt); #endif +#ifdef HAVE_HW_H264 + const struct hw_h264_api *hw_h264; +#endif }; /* plugin header */ diff --git a/apps/plugins/SUBDIRS b/apps/plugins/SUBDIRS index b035fc9f50..7c88b086be 100644 --- a/apps/plugins/SUBDIRS +++ b/apps/plugins/SUBDIRS @@ -83,6 +83,10 @@ pdbox mpegplayer #endif +#ifdef HAVE_HW_H264 +h264_player +#endif + /* Lua needs at least 160 KB to work in */ #if PLUGIN_BUFFER_SIZE >= 0x80000 lua diff --git a/apps/plugins/h264_player/SOURCES b/apps/plugins/h264_player/SOURCES new file mode 100644 index 0000000000..0cf6cd667a --- /dev/null +++ b/apps/plugins/h264_player/SOURCES @@ -0,0 +1,5 @@ +mp4_demux.c +video_audio.c +video_pcm.c +video_playback.c +h264_player.c diff --git a/apps/plugins/h264_player/h264_player.c b/apps/plugins/h264_player/h264_player.c new file mode 100644 index 0000000000..d76bf72379 --- /dev/null +++ b/apps/plugins/h264_player/h264_player.c @@ -0,0 +1,51 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "plugin.h" +#include "video_playback.h" + +enum plugin_status plugin_start(const void *parameter) +{ + const char *path = parameter; + size_t buffer_size = 0; + void *buffer; + int result; + + if (path == NULL || path[0] == '\0') + { + rb->splash(HZ * 2, "No video file"); + return PLUGIN_ERROR; + } + + buffer = rb->plugin_get_audio_buffer(&buffer_size); + if (buffer == NULL || buffer_size == 0) + { + rb->splash(HZ * 2, "Not enough video memory"); + return PLUGIN_ERROR; + } + + result = video_h264_play(path, buffer, buffer_size); + rb->button_clear_queue(); + rb->plugin_release_audio_buffer(); + + if (result == 2) + return PLUGIN_USB_CONNECTED; + return result < 0 ? PLUGIN_ERROR : PLUGIN_OK; +} diff --git a/apps/plugins/h264_player/h264_player.make b/apps/plugins/h264_player/h264_player.make new file mode 100644 index 0000000000..ae0c2e0dd6 --- /dev/null +++ b/apps/plugins/h264_player/h264_player.make @@ -0,0 +1,26 @@ +# __________ __ ___. +# Open \______ \ ____ ____ | | _\_ |__ _______ ___ +# Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / +# Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < +# Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ +# \/ \/ \/ \/ \/ +# + +H264PLAYERSRCDIR := $(APPSDIR)/plugins/h264_player +H264PLAYERBUILDDIR := $(BUILDDIR)/apps/plugins/h264_player + +ROCKS += $(H264PLAYERBUILDDIR)/h264_player.rock + +H264PLAYER_SRC := $(call preprocess, $(H264PLAYERSRCDIR)/SOURCES) +H264PLAYER_OBJ := $(call c2obj, $(H264PLAYER_SRC)) + +# add source files to OTHER_SRC to get automatic dependencies +OTHER_SRC += $(H264PLAYER_SRC) + +$(H264PLAYERBUILDDIR)/h264_player.rock: $(H264PLAYER_OBJ) + +$(H264PLAYERBUILDDIR)/%.o: $(H264PLAYERSRCDIR)/%.c \ + $(H264PLAYERSRCDIR)/h264_player.make + $(SILENT)mkdir -p $(dir $@) + $(call PRINTS,CC $(subst $(ROOTDIR)/,,$<))$(CC) -I$(dir $<) \ + $(PLUGINFLAGS) -c $< -o $@ diff --git a/apps/plugins/h264_player/mp4_demux.c b/apps/plugins/h264_player/mp4_demux.c new file mode 100644 index 0000000000..139b776bff --- /dev/null +++ b/apps/plugins/h264_player/mp4_demux.c @@ -0,0 +1,1548 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025-2026 David Cormier + * + * MP4 video demuxer - extracts H.264 video track data from MP4/M4V files. + * Based on libm4a/demux.c box-walking pattern, with fd-based I/O and + * video track support (vmhd + avc1/avcC parsing). + * + * Original demux.c copyright: + * Copyright (c) 2005 David Hammerton + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include "plugin.h" +#include "system.h" +#include "file.h" +#include "rbendian.h" +#include "mp4_demux.h" + +#include + +/* --- Simple fd-based stream I/O --- */ + +struct mp4_stream { + int fd; + off_t pos; + off_t size; + bool eof; +}; + +static void stream_init(struct mp4_stream *s, int fd, off_t size) +{ + s->fd = fd; + s->pos = 0; + s->size = size; + s->eof = false; +} + +static bool stream_read(struct mp4_stream *s, void *buf, size_t len) +{ + size_t done = 0; + + if (s->pos < 0 || s->pos > s->size || + len > (size_t)(s->size - s->pos)) + { + s->eof = true; + return false; + } + + while (done < len) + { + ssize_t count = rb->read(s->fd, (uint8_t *)buf + done, len - done); + + if (count <= 0) + { + s->eof = true; + return false; + } + done += (size_t)count; + s->pos += count; + } + return true; +} + +static uint32_t stream_read_uint32(struct mp4_stream *s) +{ + uint8_t buf[4]; + if (!stream_read(s, buf, 4)) + return 0; + /* big-endian */ + return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) + | ((uint32_t)buf[2] << 8) | (uint32_t)buf[3]; +} + +static uint16_t stream_read_uint16(struct mp4_stream *s) +{ + uint8_t buf[2]; + if (!stream_read(s, buf, 2)) + return 0; + return ((uint16_t)buf[0] << 8) | (uint16_t)buf[1]; +} + +static uint8_t stream_read_uint8(struct mp4_stream *s) +{ + uint8_t b; + if (!stream_read(s, &b, 1)) + return 0; + return b; +} + +static bool stream_read_uint32_array(struct mp4_stream *s, uint32_t *values, + uint32_t count) +{ + uint32_t i; + + if (!stream_read(s, values, (size_t)count * sizeof(*values))) + return false; + for (i = 0; i < count; i++) + values[i] = betoh32(values[i]); + return true; +} + +static void stream_skip(struct mp4_stream *s, size_t n) +{ + if (s->pos < 0 || s->pos > s->size || + n > (size_t)(s->size - s->pos)) + { + s->eof = true; + return; + } + s->pos = rb->lseek(s->fd, (off_t)n, SEEK_CUR); + if (s->pos < 0) + s->eof = true; +} + +static off_t stream_tell(struct mp4_stream *s) +{ + return s->pos; +} + +/* --- Box parsers --- */ + +struct mp4_parse_ctx { + struct mp4_stream stream; + struct mp4v_demux_res *res; + bool found_video; /* set when we find a video track */ + bool found_audio; /* set when we find an audio track */ + bool in_video_trak; /* currently inside a video trak */ + bool in_audio_trak; /* currently inside an audio trak */ + uint16_t tkhd_dw; /* tkhd display width (pending video confirm) */ + uint16_t tkhd_dh; /* tkhd display height (pending video confirm) */ + /* Pending per-trak fields (committed at end of trak once type is known) */ + uint32_t pending_timescale; /* from mdhd (parsed before hdlr) */ + uint32_t pending_lead_trim; /* from edts/elst (parsed before mdia) */ +}; + +/* Parse avcC box - H.264 decoder configuration record. + * Layout: configurationVersion(1) + AVCProfileIndication(1) + + * profile_compatibility(1) + AVCLevelIndication(1) + + * lengthSizeMinusOne(1, lower 2 bits) + + * numSPS(1, lower 5 bits) + [spsLen(2)+spsData]... + + * numPPS(1) + [ppsLen(2)+ppsData]... */ +static bool read_chunk_avcc(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t data_len; + uint8_t config_ver; + + if (chunk_len < 8) + return false; + data_len = chunk_len - 8; + if (data_len > MP4V_MAX_CODECDATA || data_len < 7) + return false; + + /* Read the entire avcC payload into codecdata */ + ctx->res->codecdata_len = data_len; + if (!stream_read(&ctx->stream, ctx->res->codecdata, data_len)) + return false; + + /* Parse key fields from the buffer */ + config_ver = ctx->res->codecdata[0]; + if (config_ver != 1) + return false; + + ctx->res->avc_profile = ctx->res->codecdata[1]; + ctx->res->avc_level = ctx->res->codecdata[3]; + ctx->res->nalu_len_size = (ctx->res->codecdata[4] & 0x03) + 1; + ctx->res->num_sps = ctx->res->codecdata[5] & 0x1F; + + /* Count PPS: skip past SPS entries to find numPPS */ + { + size_t offset = 6; + uint8_t i; + + for (i = 0; i < ctx->res->num_sps && offset + 2 <= data_len; i++) + { + uint16_t sps_len = ((uint16_t)ctx->res->codecdata[offset] << 8) + | ctx->res->codecdata[offset + 1]; + offset += 2 + sps_len; + } + + if (offset < data_len) + ctx->res->num_pps = ctx->res->codecdata[offset]; + } + + return true; +} + +/* Parse stsd for video: expects avc1 sample entry */ +static bool read_chunk_stsd_video(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries, i; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + /* version + flags */ + stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + for (i = 0; i < numentries && size_remaining > 8; i++) + { + uint32_t entry_size = stream_read_uint32(&ctx->stream); + uint32_t format = stream_read_uint32(&ctx->stream); + uint32_t entry_remaining = entry_size - 8; + + if (entry_size <= 8 || entry_size > size_remaining) + return false; + + if (format == MAKEFOURCC('a','v','c','1') + || format == MAKEFOURCC('a','v','c','3')) + { + if (entry_remaining < 78) + return false; + ctx->res->format = format; + + /* reserved(6) + data_ref_index(2) */ + stream_skip(&ctx->stream, 8); + entry_remaining -= 8; + + /* pre-defined(2) + reserved(2) + pre-defined(12) */ + stream_skip(&ctx->stream, 16); + entry_remaining -= 16; + + ctx->res->width = stream_read_uint16(&ctx->stream); + ctx->res->height = stream_read_uint16(&ctx->stream); + entry_remaining -= 4; + + /* horiz_resolution(4) + vert_resolution(4) + reserved(4) + + frame_count(2) + compressor_name(32) + depth(2) + + pre_defined(2) */ + stream_skip(&ctx->stream, 50); + entry_remaining -= 50; + + /* Walk sub-boxes looking for avcC */ + while (entry_remaining > 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len <= 8 || sub_len > entry_remaining) + break; + + if (sub_id == MAKEFOURCC('a','v','c','C')) + { + if (!read_chunk_avcc(ctx, sub_len)) + return false; + } + else + { + stream_skip(&ctx->stream, sub_len - 8); + } + + entry_remaining -= sub_len; + } + + if (entry_remaining > 0) + stream_skip(&ctx->stream, entry_remaining); + } + else + { + /* Unknown video format, skip */ + stream_skip(&ctx->stream, entry_remaining); + } + + size_remaining -= entry_size; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return ctx->res->format != 0; +} + +/* Read MPEG-4 descriptor length (variable length, 7 bits per byte, + * high bit means continue). + * Ported from libm4a/demux.c:78-92. */ +static uint32_t read_mp4_descr_length(struct mp4_stream *s) +{ + uint8_t b; + uint8_t num_bytes = 0; + uint32_t length = 0; + + do { + b = stream_read_uint8(s); + num_bytes++; + length = (length << 7) | (b & 0x7F); + } while ((b & 0x80) && num_bytes < 4); + + return length; +} + +/* Parse esds box - extract AudioSpecificConfig for AAC decoder init. + * Ported from libm4a/demux.c:95-154. */ +static bool read_chunk_esds(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + off_t box_end; + uint8_t tag; + uint32_t descr_len; + + if (chunk_len < 12) + return false; + box_end = ctx->stream.pos + (off_t)(chunk_len - 8); + + /* version + flags */ + stream_read_uint32(&ctx->stream); + + /* ES_Descriptor (tag 0x03) */ + tag = stream_read_uint8(&ctx->stream); + descr_len = read_mp4_descr_length(&ctx->stream); + (void)descr_len; + + if (tag == 0x03) + { + /* ES_ID(2) + streamPriority(1) */ + stream_skip(&ctx->stream, 3); + } + else + { + /* Non-standard: skip 2 bytes instead */ + stream_skip(&ctx->stream, 2); + } + + /* DecoderConfigDescriptor (tag 0x04) */ + tag = stream_read_uint8(&ctx->stream); + descr_len = read_mp4_descr_length(&ctx->stream); + (void)descr_len; + + if (tag != 0x04) + { + off_t remaining = box_end - ctx->stream.pos; + + if (remaining < 0) + return false; + stream_skip(&ctx->stream, (size_t)remaining); + return !ctx->stream.eof; + } + + /* objectTypeIndication(1) + streamType(1) + bufferSizeDB(3) + + maxBitrate(4) + avgBitrate(4) = 13 bytes */ + stream_skip(&ctx->stream, 13); + + /* DecoderSpecificInfo (tag 0x05) = AudioSpecificConfig */ + tag = stream_read_uint8(&ctx->stream); + descr_len = read_mp4_descr_length(&ctx->stream); + + if (tag == 0x05 && descr_len > 0) + { + if (descr_len > sizeof(ctx->res->audio_codecdata)) + descr_len = sizeof(ctx->res->audio_codecdata); + + ctx->res->audio_codecdata_len = descr_len; + stream_read(&ctx->stream, ctx->res->audio_codecdata, descr_len); + } + + /* Skip any trailing descriptors (SLConfigDescriptor etc.) */ + { + off_t remaining = box_end - ctx->stream.pos; + if (remaining > 0) + stream_skip(&ctx->stream, (size_t)remaining); + } + + return !ctx->stream.eof && ctx->stream.pos == box_end; +} + +/* Parse stsd for audio: expects mp4a sample entry. + * mp4a layout: reserved(6) + data_ref_index(2) + reserved(8) + + * channels(2) + sample_size(2) + packet_size(2) + sample_rate(4, 16.16 FP) + + * reserved(2) + sub-boxes (esds, etc.) */ +static bool read_chunk_stsd_audio(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries, i; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + for (i = 0; i < numentries && size_remaining > 8; i++) + { + uint32_t entry_size = stream_read_uint32(&ctx->stream); + uint32_t format = stream_read_uint32(&ctx->stream); + uint32_t entry_remaining = entry_size - 8; + + if (entry_size <= 8 || entry_size > size_remaining) + return false; + + if (format == MAKEFOURCC('m','p','4','a')) + { + if (entry_remaining < 28) + return false; + ctx->res->audio_format = format; + + /* reserved(6) + data_ref_index(2) */ + stream_skip(&ctx->stream, 8); + entry_remaining -= 8; + + /* reserved(8) */ + stream_skip(&ctx->stream, 8); + entry_remaining -= 8; + + /* channelCount(2) + sampleSize(2) + compressionID(2) + + packetSize(2) = 8 bytes */ + stream_skip(&ctx->stream, 8); + entry_remaining -= 8; + + /* sampleRate: 16.16 fixed-point, 4 bytes */ + { + uint32_t sr_fp = stream_read_uint32(&ctx->stream); + ctx->res->audio_sample_rate = sr_fp >> 16; + } + entry_remaining -= 4; + + /* Walk sub-boxes for esds */ + while (entry_remaining > 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len <= 8 || sub_len > entry_remaining) + break; + + if (sub_id == MAKEFOURCC('e','s','d','s')) + { + if (!read_chunk_esds(ctx, sub_len)) + return false; + } + else + { + stream_skip(&ctx->stream, sub_len - 8); + } + + entry_remaining -= sub_len; + } + + if (entry_remaining > 0) + stream_skip(&ctx->stream, entry_remaining); + } + else + { + stream_skip(&ctx->stream, entry_remaining); + } + + size_remaining -= entry_size; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +static bool read_chunk_stsz_audio(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t default_size; + uint32_t numsizes; + + if (chunk_len < 20) + return false; + size_remaining = chunk_len - 8; + + stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + default_size = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + numsizes = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (default_size == 0 && numsizes > size_remaining / 4) + return false; + ctx->res->audio_num_samples = numsizes; + stream_skip(&ctx->stream, size_remaining); + return !ctx->stream.eof; +} + +static bool read_audio_table_count(struct mp4_parse_ctx *ctx, + size_t chunk_len, size_t entry_size, + uint32_t *count) +{ + size_t size_remaining; + uint32_t numentries; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); + size_remaining -= 4; + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + if (entry_size == 0 || numentries > size_remaining / entry_size) + return false; + *count = numentries; + stream_skip(&ctx->stream, size_remaining); + return !ctx->stream.eof; +} + +static bool read_chunk_stsc_audio(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + return read_audio_table_count(ctx, chunk_len, 12, + &ctx->res->audio_num_stsc); +} + +static bool read_chunk_stco_audio(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + return read_audio_table_count(ctx, chunk_len, 4, + &ctx->res->audio_num_stco); +} + +static bool read_chunk_co64_audio(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + return read_audio_table_count(ctx, chunk_len, 8, + &ctx->res->audio_num_stco); +} + +/* Audio sample table container */ +static bool read_chunk_stbl_audio(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + switch (sub_id) + { + case MAKEFOURCC('s','t','s','d'): + if (!read_chunk_stsd_audio(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','s','z'): + if (!read_chunk_stsz_audio(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','s','c'): + if (!read_chunk_stsc_audio(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','c','o'): + if (!read_chunk_stco_audio(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('c','o','6','4'): + if (!read_chunk_co64_audio(ctx, sub_len)) + return false; + break; + /* No stss for audio; all AAC frames are sync samples */ + default: + stream_skip(&ctx->stream, sub_len - 8); + break; + } + + size_remaining -= sub_len; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +/* stts - time-to-sample table */ +static bool read_chunk_stts(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries, i; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (numentries > MP4V_MAX_STTS || numentries > size_remaining / 8) + return false; + + ctx->res->num_stts = numentries; + if (!stream_read(&ctx->stream, ctx->res->stts, + (size_t)numentries * sizeof(ctx->res->stts[0]))) + return false; + size_remaining -= (size_t)numentries * 8; + for (i = 0; i < numentries; i++) + { + ctx->res->stts[i].sample_count = + betoh32(ctx->res->stts[i].sample_count); + ctx->res->stts[i].sample_delta = + betoh32(ctx->res->stts[i].sample_delta); + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* stsz - sample sizes */ +static bool read_chunk_stsz(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t default_size, numsizes, i; + + if (chunk_len < 20) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + default_size = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + numsizes = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (default_size == 0 && numsizes > size_remaining / 4) + return false; + ctx->res->num_samples = numsizes; + + if (default_size != 0) + { + /* All samples same size */ + uint32_t cap = ctx->res->sample_sizes == NULL ? 0 : + MIN(numsizes, ctx->res->sample_sizes_cap); + for (i = 0; i < cap; i++) + ctx->res->sample_sizes[i] = default_size; + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + return true; + } + + /* Variable sample sizes */ + { + uint32_t cap = ctx->res->sample_sizes == NULL ? 0 : + MIN(numsizes, ctx->res->sample_sizes_cap); + if (!stream_read_uint32_array(&ctx->stream, + ctx->res->sample_sizes, cap)) + return false; + size_remaining -= (size_t)cap * 4; + /* Skip remaining if we hit our cap */ + if (numsizes > cap) + { + size_t skip = (size_t)(numsizes - cap) * 4; + + stream_skip(&ctx->stream, skip); + size_remaining -= skip; + } + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* stsc - sample-to-chunk table */ +static bool read_chunk_stsc(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries, i, fill; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (numentries > size_remaining / 12) + return false; + ctx->res->num_stsc = numentries; + fill = ctx->res->stsc == NULL ? 0 : + MIN(numentries, ctx->res->stsc_cap); + + if (!stream_read(&ctx->stream, ctx->res->stsc, + (size_t)fill * sizeof(ctx->res->stsc[0]))) + return false; + size_remaining -= (size_t)fill * 12; + for (i = 0; i < fill; i++) + { + ctx->res->stsc[i].first_chunk = + betoh32(ctx->res->stsc[i].first_chunk); + ctx->res->stsc[i].samples_per_chunk = + betoh32(ctx->res->stsc[i].samples_per_chunk); + ctx->res->stsc[i].sample_desc_index = + betoh32(ctx->res->stsc[i].sample_desc_index); + } + + if (numentries > fill) + { + size_t skip = (size_t)(numentries - fill) * 12; + + stream_skip(&ctx->stream, skip); + size_remaining -= skip; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* stco - chunk offset table */ +static bool read_chunk_stco(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries, cap; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (numentries > size_remaining / 4) + return false; + ctx->res->num_stco = numentries; /* real count from file */ + cap = ctx->res->chunk_offsets == NULL ? 0 : + MIN(numentries, ctx->res->chunk_offsets_cap); + + if (!stream_read_uint32_array(&ctx->stream, + ctx->res->chunk_offsets, cap)) + return false; + size_remaining -= (size_t)cap * 4; + + if (numentries > cap) + { + size_t skip = (size_t)(numentries - cap) * 4; + + stream_skip(&ctx->stream, skip); + size_remaining -= skip; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* co64 - 64-bit chunk offset table (for files >4GB). + * Reads 64-bit offsets but truncates to 32-bit. + * Files with chunk offsets >4GB are not supported. */ +static bool read_chunk_co64(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries, i, cap; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (numentries > size_remaining / 8) + return false; + ctx->res->num_stco = numentries; /* real count from file */ + cap = ctx->res->chunk_offsets == NULL ? 0 : + MIN(numentries, ctx->res->chunk_offsets_cap); + + for (i = 0; i < cap; i++) + { + uint32_t hi = stream_read_uint32(&ctx->stream); + uint32_t lo = stream_read_uint32(&ctx->stream); + if (hi != 0) + return false; + ctx->res->chunk_offsets[i] = lo; + size_remaining -= 8; + } + + if (numentries > cap) + { + size_t skip = (size_t)(numentries - cap) * 8; + + stream_skip(&ctx->stream, skip); + size_remaining -= skip; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* stss - sync sample (keyframe) table */ +static bool read_chunk_stss(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries; + uint32_t cap; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (numentries > size_remaining / 4) + return false; + ctx->res->num_stss = numentries; + cap = ctx->res->sync_samples == NULL ? 0 : + MIN(numentries, ctx->res->sync_samples_cap); + + if (!stream_read_uint32_array(&ctx->stream, + ctx->res->sync_samples, cap)) + return false; + size_remaining -= (size_t)cap * 4; + + if (numentries > cap) + { + size_t skip = (size_t)(numentries - cap) * 4; + + stream_skip(&ctx->stream, skip); + size_remaining -= skip; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* stbl - sample table container */ +static bool read_chunk_stbl(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + switch (sub_id) + { + case MAKEFOURCC('s','t','s','d'): + if (!read_chunk_stsd_video(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','t','s'): + if (!read_chunk_stts(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','s','z'): + if (!read_chunk_stsz(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','s','c'): + if (!read_chunk_stsc(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','c','o'): + if (!read_chunk_stco(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('c','o','6','4'): + if (!read_chunk_co64(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','s','s'): + if (!read_chunk_stss(ctx, sub_len)) + return false; + break; + default: + stream_skip(&ctx->stream, sub_len - 8); + break; + } + + size_remaining -= sub_len; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +/* minf - accepts vmhd (video) instead of smhd (audio) */ +static bool read_chunk_minf(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + /* Walk sub-boxes, look for vmhd to confirm video, then find stbl */ + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + switch (sub_id) + { + case MAKEFOURCC('v','m','h','d'): + /* Video media header confirms this is a video track */ + ctx->in_video_trak = true; + stream_skip(&ctx->stream, sub_len - 8); + break; + case MAKEFOURCC('s','m','h','d'): + /* Sound media header confirms this is an audio track */ + ctx->in_video_trak = false; + ctx->in_audio_trak = true; + stream_skip(&ctx->stream, sub_len - 8); + break; + case MAKEFOURCC('s','t','b','l'): + if (ctx->in_video_trak) + { + if (!read_chunk_stbl(ctx, sub_len)) + return false; + } + else if (ctx->in_audio_trak) + { + if (!read_chunk_stbl_audio(ctx, sub_len)) + return false; + } + else + { + stream_skip(&ctx->stream, sub_len - 8); + } + break; + default: + stream_skip(&ctx->stream, sub_len - 8); + break; + } + + size_remaining -= sub_len; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +/* mdhd - media header (extract timescale) */ +static bool read_chunk_mdhd(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t version; + + if (chunk_len < 28) + return false; + size_remaining = chunk_len - 8; + version = stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + { + uint32_t ts; + + if ((version >> 24) == 0) + { + /* Version 0: 4-byte fields */ + stream_skip(&ctx->stream, 4); /* creation_time */ + stream_skip(&ctx->stream, 4); /* modification_time */ + ts = stream_read_uint32(&ctx->stream); + stream_skip(&ctx->stream, 4); /* duration */ + size_remaining -= 16; + } + else if ((version >> 24) == 1) + { + if (chunk_len < 40) + return false; + /* Version 1: 8-byte fields */ + stream_skip(&ctx->stream, 8); /* creation_time */ + stream_skip(&ctx->stream, 8); /* modification_time */ + ts = stream_read_uint32(&ctx->stream); + stream_skip(&ctx->stream, 8); /* duration */ + size_remaining -= 28; + } + else + { + return false; + } + + /* Save pending; commit at end of trak once type is known. + * mdhd comes before hdlr in mdia, so track type isn't known yet. */ + ctx->pending_timescale = ts; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* hdlr - handler reference (check for 'vide') */ +static bool read_chunk_hdlr(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t handler_type; + + if (chunk_len < 20) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + stream_read_uint32(&ctx->stream); /* pre_defined */ + handler_type = stream_read_uint32(&ctx->stream); + size_remaining -= 12; + + ctx->in_video_trak = false; + ctx->in_audio_trak = false; + + if (handler_type == MAKEFOURCC('v','i','d','e')) + ctx->in_video_trak = true; + else if (handler_type == MAKEFOURCC('s','o','u','n')) + ctx->in_audio_trak = true; + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* mdia - media container */ +static bool read_chunk_mdia(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + switch (sub_id) + { + case MAKEFOURCC('m','d','h','d'): + if (!read_chunk_mdhd(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('h','d','l','r'): + if (!read_chunk_hdlr(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('m','i','n','f'): + if (!read_chunk_minf(ctx, sub_len)) + return false; + break; + default: + stream_skip(&ctx->stream, sub_len - 8); + break; + } + + size_remaining -= sub_len; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +/* tkhd - track header (extract display width/height for PAR) */ +static bool read_chunk_tkhd(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t version; + uint32_t dw_fp, dh_fp; /* 16.16 fixed-point display dimensions */ + + if (chunk_len < 92) + return false; + size_remaining = chunk_len - 8; + version = stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + if ((version >> 24) == 0) + { + /* Version 0: creation(4)+modification(4)+track_id(4)+reserved(4)+ + duration(4) = 20 bytes, then reserved(8)+layer(2)+alt_group(2)+ + volume(2)+reserved(2)+matrix(36)+width(4)+height(4) = 56 bytes */ + stream_skip(&ctx->stream, 20 + 8 + 2 + 2 + 2 + 2 + 36); + size_remaining -= 20 + 8 + 2 + 2 + 2 + 2 + 36; + } + else if ((version >> 24) == 1) + { + if (chunk_len < 104) + return false; + /* Version 1: creation(8)+modification(8)+track_id(4)+reserved(4)+ + duration(8) = 32 bytes */ + stream_skip(&ctx->stream, 32 + 8 + 2 + 2 + 2 + 2 + 36); + size_remaining -= 32 + 8 + 2 + 2 + 2 + 2 + 36; + } + else + { + return false; + } + + dw_fp = stream_read_uint32(&ctx->stream); /* width in 16.16 */ + dh_fp = stream_read_uint32(&ctx->stream); /* height in 16.16 */ + size_remaining -= 8; + + /* Commit to res only after mdia confirms video */ + ctx->tkhd_dw = (uint16_t)(dw_fp >> 16); + ctx->tkhd_dh = (uint16_t)(dh_fp >> 16); + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* edts/elst - edit list (extract media_time for encoder priming offset) */ +static bool read_chunk_elst(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t version_flags; + uint32_t entry_count, i; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + version_flags = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + entry_count = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + { + size_t entry_size = ((version_flags >> 24) == 0) ? 12 : 20; + + if ((version_flags >> 24) > 1 || + entry_count > size_remaining / entry_size) + return false; + for (i = 0; i < entry_count; i++) + { + int32_t media_time; + + if ((version_flags >> 24) == 0) + { + stream_skip(&ctx->stream, 4); /* segment_duration */ + media_time = (int32_t)stream_read_uint32(&ctx->stream); + stream_skip(&ctx->stream, 4); /* media_rate (16.16) */ + size_remaining -= 12; + } + else + { + stream_skip(&ctx->stream, 8); /* segment_duration (64-bit) */ + stream_skip(&ctx->stream, 4); /* media_time high */ + media_time = (int32_t)stream_read_uint32(&ctx->stream); + stream_skip(&ctx->stream, 4); /* media_rate */ + size_remaining -= 20; + } + + /* First non-empty edit: media_time > 0 = priming offset. + * Save until the track type is known. */ + if (i == 0 && media_time > 0) + ctx->pending_lead_trim = (uint32_t)media_time; + } + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* edts container: walk for elst */ +static bool read_chunk_edts(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + if (sub_id == MAKEFOURCC('e','l','s','t')) + { + if (!read_chunk_elst(ctx, sub_len)) + return false; + } + else + { + stream_skip(&ctx->stream, sub_len - 8); + } + + size_remaining -= sub_len; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +/* trak - track container */ +static bool read_chunk_trak(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + /* Reset per-track state (pending fields MUST be reset to prevent + * stale values from one trak leaking into the next) */ + ctx->in_video_trak = false; + ctx->in_audio_trak = false; + ctx->pending_timescale = 0; + ctx->pending_lead_trim = 0; + ctx->tkhd_dw = 0; + ctx->tkhd_dh = 0; + + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + switch (sub_id) + { + case MAKEFOURCC('t','k','h','d'): + if (!read_chunk_tkhd(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('m','d','i','a'): + if (!read_chunk_mdia(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('e','d','t','s'): + /* edts comes before mdia, so the track type is unknown. + * Parse unconditionally, save to pending_lead_trim. + * Committed to audio_lead_trim at end of trak. */ + if (!read_chunk_edts(ctx, sub_len)) + return false; + break; + default: + stream_skip(&ctx->stream, sub_len - 8); + break; + } + + size_remaining -= sub_len; + } + + /* Commit pending per-trak fields now that track type is known. + * mdhd timescale and edts/elst are parsed before hdlr identifies + * the track type, so they were saved to pending fields above. */ + if (ctx->in_video_trak && ctx->res->format != 0) + { + ctx->found_video = true; + ctx->res->display_width = ctx->tkhd_dw; + ctx->res->display_height = ctx->tkhd_dh; + ctx->res->timescale = ctx->pending_timescale; + } + + if (ctx->in_audio_trak && ctx->res->audio_format != 0) + { + ctx->found_audio = true; + ctx->res->audio_lead_trim = ctx->pending_lead_trim; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +/* moov - movie container */ +static bool read_chunk_moov(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + switch (sub_id) + { + case MAKEFOURCC('t','r','a','k'): + /* Parse until we have both video and audio (or all traks) */ + if (!ctx->found_video || !ctx->found_audio) + { + if (!read_chunk_trak(ctx, sub_len)) + return false; + } + else + { + stream_skip(&ctx->stream, sub_len - 8); + } + break; + default: + stream_skip(&ctx->stream, sub_len - 8); + break; + } + + size_remaining -= sub_len; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +int mp4v_demux_open(const char *filepath, + struct mp4v_demux_res *res, + uint32_t *sample_buf, uint32_t sample_cap, + uint32_t *chunk_buf, uint32_t chunk_cap, + struct mp4v_stsc_entry *stsc_buf, uint32_t stsc_cap, + uint32_t *sync_buf, uint32_t sync_cap) +{ + struct mp4_parse_ctx ctx; + off_t file_size; + int fd; + + if (filepath == NULL || res == NULL) + return -1; + fd = rb->open(filepath, O_RDONLY); + if (fd < 0) + return -1; + file_size = rb->filesize(fd); + if (file_size <= 0 || (uint64_t)file_size > UINT32_MAX) + { + rb->close(fd); + return -1; + } + + rb->memset(res, 0, sizeof(*res)); + res->sample_sizes = sample_buf; + res->sample_sizes_cap = sample_cap; + res->chunk_offsets = chunk_buf; + res->chunk_offsets_cap = chunk_cap; + res->stsc = stsc_buf; + res->stsc_cap = stsc_cap; + res->sync_samples = sync_buf; + res->sync_samples_cap = sync_cap; + res->file_size = (uint32_t)file_size; + + rb->memset(&ctx, 0, sizeof(ctx)); + stream_init(&ctx.stream, fd, file_size); + ctx.res = res; + + /* Walk top-level boxes */ + while (ctx.stream.pos < file_size && !ctx.stream.eof) + { + uint32_t chunk_len = stream_read_uint32(&ctx.stream); + uint32_t chunk_id; + + if (ctx.stream.eof) + break; + + if (chunk_len < 8) + { + rb->close(fd); + return -1; + } + + chunk_id = stream_read_uint32(&ctx.stream); + if (ctx.stream.eof || + chunk_len - 8 > (uint32_t)(file_size - ctx.stream.pos)) + { + rb->close(fd); + return -1; + } + + switch (chunk_id) + { + case MAKEFOURCC('f','t','y','p'): + stream_skip(&ctx.stream, chunk_len - 8); + break; + case MAKEFOURCC('m','o','o','v'): + if (!read_chunk_moov(&ctx, chunk_len)) + { + rb->close(fd); + return -1; + } + break; + case MAKEFOURCC('m','d','a','t'): + if (chunk_len > 8) + { + res->mdat_offset = (uint32_t)stream_tell(&ctx.stream); + res->mdat_len = chunk_len - 8; + } + /* If we already found everything we need, we're done */ + if (ctx.found_video && ctx.found_audio) + goto done; + stream_skip(&ctx.stream, chunk_len - 8); + break; + default: + stream_skip(&ctx.stream, chunk_len - 8); + break; + } + } + +done: + rb->close(fd); + + if (ctx.stream.eof || !ctx.found_video || res->format == 0) + return -1; + + return 0; +} + +int mp4v_get_sample_offset(const struct mp4v_demux_res *res, + uint32_t sample_index, + uint32_t *offset_out, + uint32_t *size_out) +{ + uint32_t chunk_index = 0; + uint32_t sample_in_chunk = 0; + uint64_t samples_so_far = 0; + uint64_t chunk_offset; + uint32_t i; + bool found = false; + + if (res == NULL || offset_out == NULL || size_out == NULL || + res->sample_sizes == NULL || res->chunk_offsets == NULL || + res->stsc == NULL || sample_index >= res->num_samples || + sample_index >= res->sample_sizes_cap || + res->num_stsc > res->stsc_cap || + res->num_stco > res->chunk_offsets_cap) + return -1; + + *size_out = res->sample_sizes[sample_index]; + + /* Walk stsc to find which chunk contains this sample */ + for (i = 0; i < res->num_stsc; i++) + { + uint32_t first_chunk; + uint32_t spc = res->stsc[i].samples_per_chunk; + uint32_t next_first; + + if (res->stsc[i].first_chunk == 0 || spc == 0) + return -1; + first_chunk = res->stsc[i].first_chunk - 1; + if (i + 1 < res->num_stsc) + { + if (res->stsc[i + 1].first_chunk == 0) + return -1; + next_first = res->stsc[i + 1].first_chunk - 1; + } + else + next_first = res->num_stco; + + if (next_first <= first_chunk || next_first > res->num_stco) + return -1; + uint32_t chunks_in_run = next_first - first_chunk; + uint64_t samples_in_run = (uint64_t)chunks_in_run * spc; + + if (samples_so_far + samples_in_run > sample_index) + { + /* Sample is within this stsc run */ + uint32_t sample_offset_in_run = sample_index - samples_so_far; + chunk_index = first_chunk + (sample_offset_in_run / spc); + sample_in_chunk = sample_offset_in_run % spc; + found = true; + break; + } + + samples_so_far += samples_in_run; + } + + if (!found || chunk_index >= res->num_stco) + return -1; + + /* Get the chunk's file offset */ + chunk_offset = res->chunk_offsets[chunk_index]; + + /* Add offsets for preceding samples within this chunk */ + { + uint32_t first_sample_in_chunk = sample_index - sample_in_chunk; + uint32_t j; + + for (j = 0; j < sample_in_chunk; j++) + { + uint32_t index = first_sample_in_chunk + j; + + if (index >= res->num_samples || index >= res->sample_sizes_cap) + return -1; + chunk_offset += res->sample_sizes[index]; + } + } + + if (chunk_offset > UINT32_MAX || + chunk_offset + *size_out > res->file_size) + return -1; + *offset_out = (uint32_t)chunk_offset; + return 0; +} + +bool mp4v_is_keyframe(const struct mp4v_demux_res *res, + uint32_t sample_index) +{ + uint32_t i; + uint32_t sample_num = sample_index + 1; /* stss is 1-based */ + + /* No stss table means all samples are sync samples */ + if (res->num_stss == 0) + return true; + if (res->sync_samples == NULL || + res->sync_samples_cap < res->num_stss) + return false; + + for (i = 0; i < res->num_stss; i++) + { + if (res->sync_samples[i] == sample_num) + return true; + if (res->sync_samples[i] > sample_num) + break; + } + + return false; +} diff --git a/apps/plugins/h264_player/mp4_demux.h b/apps/plugins/h264_player/mp4_demux.h new file mode 100644 index 0000000000..33d2e02a4d --- /dev/null +++ b/apps/plugins/h264_player/mp4_demux.h @@ -0,0 +1,150 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025-2026 David Cormier + * + * H.264 player MP4 demuxer - extracts video track data from MP4/M4V files. + * Forked from libm4a/demux.c, adapted for fd-based I/O and video tracks. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef MP4_DEMUX_H +#define MP4_DEMUX_H + +#include +#include +#include + +/* Max SPS/PPS codec data from avcC box */ +#define MP4V_MAX_CODECDATA 512 + +#define MP4V_MAX_STTS 4096 + +#ifndef MAKEFOURCC +#define MAKEFOURCC(ch0, ch1, ch2, ch3) ( \ + ( (int32_t)(char)(ch0) << 24 ) | \ + ( (int32_t)(char)(ch1) << 16 ) | \ + ( (int32_t)(char)(ch2) << 8 ) | \ + ( (int32_t)(char)(ch3) ) ) +#endif + +struct mp4v_stts_entry { + uint32_t sample_count; + uint32_t sample_delta; +}; + +struct mp4v_stsc_entry { + uint32_t first_chunk; + uint32_t samples_per_chunk; + uint32_t sample_desc_index; +}; + +struct mp4v_demux_res { + /* Video format */ + uint32_t format; /* fourcc: 'avc1' etc. */ + uint16_t width; /* coded width from avc1 sample entry */ + uint16_t height; /* coded height from avc1 sample entry */ + uint16_t display_width; /* display width from tkhd (PAR-adjusted) */ + uint16_t display_height; /* display height from tkhd */ + uint32_t timescale; /* from mdhd */ + + /* avcC decoder config (SPS + PPS) */ + uint8_t avc_profile; + uint8_t avc_level; + uint8_t nalu_len_size; /* length field size (1-4 bytes, typically 4) */ + uint8_t num_sps; + uint8_t num_pps; + uint32_t codecdata_len; + uint8_t codecdata[MP4V_MAX_CODECDATA]; /* raw avcC data */ + + /* Sample sizes (stsz) */ + uint32_t num_samples; + uint32_t *sample_sizes; /* caller-provided buffer */ + uint32_t sample_sizes_cap; + + /* Time-to-sample (stts) */ + uint32_t num_stts; + struct mp4v_stts_entry stts[MP4V_MAX_STTS]; + + /* Sample-to-chunk (stsc) */ + uint32_t num_stsc; + struct mp4v_stsc_entry *stsc; /* caller-provided buffer */ + uint32_t stsc_cap; + + /* Chunk offsets (stco) */ + uint32_t num_stco; + uint32_t *chunk_offsets; /* caller-provided buffer */ + uint32_t chunk_offsets_cap; + + /* Sync samples / keyframes (stss) */ + uint32_t num_stss; + uint32_t *sync_samples; /* caller-provided buffer */ + uint32_t sync_samples_cap; + + /* File and mdat location */ + uint32_t file_size; + uint32_t mdat_offset; + uint32_t mdat_len; + + /* Audio track (populated when mp4a trak found) */ + uint32_t audio_format; /* fourcc: 'mp4a' or 0 if no audio */ + uint32_t audio_sample_rate; /* from mp4a sample entry */ + uint8_t audio_codecdata[64]; /* AudioSpecificConfig from esds */ + uint32_t audio_codecdata_len; + uint32_t audio_lead_trim; /* encoder priming from edts/elst */ + + /* Audio table counts used to size aac.codec's workspace. The AAC codec + * parses its own table data from the file. */ + uint32_t audio_num_samples; + uint32_t audio_num_stsc; + uint32_t audio_num_stco; +}; + +/* Open and parse an MP4 file, extracting video and audio tracks. + * + * filepath: path to the .mp4/.m4v file + * res: output struct (caller allocates) + * sample_buf: buffer for video sample_sizes array + * sample_cap: number of uint32_t entries in sample_buf + * chunk_buf: buffer for video chunk_offsets array + * chunk_cap: number of uint32_t entries in chunk_buf + * stsc_buf: buffer for video sample-to-chunk entries + * stsc_cap: number of entries in stsc_buf + * sync_buf: buffer for video sync-sample entries + * sync_cap: number of entries in sync_buf + * + * Returns 0 on success (video found), -1 on error. + * Audio track is optional; check res->audio_format != 0. */ +int mp4v_demux_open(const char *filepath, + struct mp4v_demux_res *res, + uint32_t *sample_buf, uint32_t sample_cap, + uint32_t *chunk_buf, uint32_t chunk_cap, + struct mp4v_stsc_entry *stsc_buf, uint32_t stsc_cap, + uint32_t *sync_buf, uint32_t sync_cap); + +/* Get the file offset and size of video sample N (0-based). + * Returns 0 on success, -1 on error. */ +int mp4v_get_sample_offset(const struct mp4v_demux_res *res, + uint32_t sample_index, + uint32_t *offset_out, + uint32_t *size_out); + +/* Check if video sample N is a keyframe (sync sample). + * Returns true if it's a keyframe or if no stss table exists + * (meaning all samples are sync samples). */ +bool mp4v_is_keyframe(const struct mp4v_demux_res *res, + uint32_t sample_index); + +#endif /* MP4_DEMUX_H */ diff --git a/apps/plugins/h264_player/video_audio.c b/apps/plugins/h264_player/video_audio.c new file mode 100644 index 0000000000..2f8434377e --- /dev/null +++ b/apps/plugins/h264_player/video_audio.c @@ -0,0 +1,563 @@ +/*************************************************************************** + * AAC audio service for the native S5L8702 H.264 player. + * + * The decoder itself remains Rockbox's normal dynamically-loaded aac.codec. + * That is important on iPod 6G: FAAD's hot code and working state are an IRAM + * overlay and cannot be linked into the permanent core without overflowing + * IRAM. This file borrows the existing codec thread, supplies file-buffer + * callbacks for the MP4, and sends decoded PCM to video_pcm. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ +#include "plugin.h" + +#ifdef HAVE_HW_H264 + +#include + +#include "codec_thread.h" +#include "codecs.h" +#include "file.h" +#include "kernel.h" +#include "metadata.h" +#include "mp4_demux.h" +#include "video_audio.h" +#include "video_pcm.h" + +#include +#include + +#define AUDIO_FILE_BUFFER_SIZE 8192 +#define AUDIO_PCM_FRAMES 1024 +#define AUDIO_START_BUFFER_MS 1000 +/* libm4a's sample_offset_t stores a uint32_t sample and file offset. Keep a + * normal CODEC_SIZE of headroom after the complete Apple chunk map. */ +#define AUDIO_CHUNK_LOOKUP_ENTRY_SIZE (2u * sizeof(uint32_t)) + +static struct codec_api video_ci; +static struct mp3entry video_id3; +static int audio_fd = -1; +static off_t audio_file_size; +static uint8_t audio_file_buffer[AUDIO_FILE_BUFFER_SIZE]; +static off_t audio_file_buffer_offset; +static size_t audio_file_buffer_length; +static int16_t audio_pcm[AUDIO_PCM_FRAMES * 2]; +static void *audio_codec_workspace; +static size_t audio_codec_workspace_size; +static uint32_t audio_sample_rate; +static int audio_sample_depth; +static int audio_stereo_mode; +static volatile long audio_action; +static volatile uint32_t audio_seek_target_ms; +static volatile bool audio_paused; +static volatile bool audio_is_ready; +static volatile bool audio_is_playing; +static volatile bool audio_has_error; +static bool audio_initialized; + +size_t video_audio_workspace_size(const struct mp4v_demux_res *demux) +{ + size_t table_size; + + if (demux == NULL || demux->audio_num_samples == 0 || + demux->audio_num_stco == 0 || + demux->audio_num_stco > + (SIZE_MAX - CODEC_SIZE) / AUDIO_CHUNK_LOOKUP_ENTRY_SIZE) + return 0; + table_size = (size_t)demux->audio_num_stco * + AUDIO_CHUNK_LOOKUP_ENTRY_SIZE; + return CODEC_SIZE + table_size; +} + +static int16_t audio_clip16(int32_t value) +{ + if (value > 32767) + return 32767; + if (value < -32768) + return -32768; + return (int16_t)value; +} + +static void *audio_codec_get_buffer(size_t *size) +{ + *size = audio_codec_workspace_size; + return audio_codec_workspace; +} + +static bool audio_refill(off_t position) +{ + ssize_t count; + + if (position < 0 || position > audio_file_size || + rb->lseek(audio_fd, position, SEEK_SET) < 0) + return false; + count = rb->read(audio_fd, audio_file_buffer, sizeof(audio_file_buffer)); + if (count < 0) + return false; + audio_file_buffer_offset = position; + audio_file_buffer_length = (size_t)count; + return true; +} + +static size_t audio_read_filebuf(void *destination, size_t size) +{ + size_t done = 0; + + while (done < size && video_ci.curpos < audio_file_size) + { + size_t in_buffer; + size_t chunk; + + if (video_ci.curpos < audio_file_buffer_offset || + video_ci.curpos >= audio_file_buffer_offset + + (off_t)audio_file_buffer_length) + { + if (!audio_refill(video_ci.curpos)) + break; + } + in_buffer = (size_t)(video_ci.curpos - audio_file_buffer_offset); + chunk = audio_file_buffer_length - in_buffer; + if (chunk > size - done) + chunk = size - done; + if (chunk == 0) + break; + rb->memcpy((uint8_t *)destination + done, + audio_file_buffer + in_buffer, chunk); + video_ci.curpos += chunk; + done += chunk; + } + return done; +} + +static void *audio_request_buffer(size_t *real_size, size_t requested) +{ + size_t available; + size_t in_buffer; + size_t needed; + off_t remaining; + + if (video_ci.curpos >= audio_file_size) + { + *real_size = 0; + return audio_file_buffer; + } + + remaining = audio_file_size - video_ci.curpos; + needed = requested; + if ((off_t)needed > remaining) + needed = (size_t)remaining; + + if (video_ci.curpos < audio_file_buffer_offset || + video_ci.curpos >= audio_file_buffer_offset + + (off_t)audio_file_buffer_length) + { + if (!audio_refill(video_ci.curpos)) + { + *real_size = 0; + return audio_file_buffer; + } + } + in_buffer = (size_t)(video_ci.curpos - audio_file_buffer_offset); + available = audio_file_buffer_length - in_buffer; + + /* codec_api.request_buffer promises the requested span whenever that + * many file bytes remain. Do not return the short tail of our 8 KiB + * cache: FAAD treats a truncated AAC frame as a fatal decode error. */ + if (available < needed) + { + if (!audio_refill(video_ci.curpos)) + { + *real_size = 0; + return audio_file_buffer; + } + in_buffer = 0; + available = audio_file_buffer_length; + } + + if (available > needed) + available = needed; + *real_size = available; + return audio_file_buffer + in_buffer; +} + +static void audio_advance_buffer(size_t amount) +{ + off_t remaining = audio_file_size - video_ci.curpos; + + if ((off_t)amount > remaining) + amount = remaining > 0 ? (size_t)remaining : 0; + video_ci.curpos += amount; + video_id3.offset = video_ci.curpos; +} + +static bool audio_seek_buffer(size_t position) +{ + if ((off_t)position > audio_file_size) + return false; + video_ci.curpos = position; + video_id3.offset = position; + return true; +} + +static void audio_seek_complete(void) +{ +} + +static void audio_set_elapsed(unsigned long elapsed) +{ + video_id3.elapsed = elapsed; +} + +static void audio_set_offset(size_t offset) +{ + video_id3.offset = offset; +} + +static void audio_configure(int setting, intptr_t value) +{ + switch (setting) + { + case DSP_SET_FREQUENCY: + if (value > 0) + audio_sample_rate = (uint32_t)value; + break; + case DSP_SET_SAMPLE_DEPTH: + audio_sample_depth = (int)value; + break; + case DSP_SET_STEREO_MODE: + audio_stereo_mode = (int)value; + break; + default: + break; + } +} + +static long audio_get_command(intptr_t *parameter) +{ + long action; + + while (audio_paused && audio_action == CODEC_ACTION_NULL) + rb->sleep(1); + rb->yield(); + action = audio_action; + if (action == CODEC_ACTION_SEEK_TIME) + { + if (parameter != NULL) + *parameter = (intptr_t)audio_seek_target_ms; + /* A seek is a one-shot command. HALT deliberately remains latched + * until codec teardown so the producer cannot resume behind us. */ + audio_action = CODEC_ACTION_NULL; + } + return action; +} + +static bool audio_loop_track(void) +{ + return false; +} + +static void audio_strip_filesize(off_t size) +{ + video_ci.filesize = size; +} + +static int32_t audio_sample_value(const void *channel, int index) +{ + if (audio_sample_depth <= 16) + return ((const int16_t *)channel)[index]; + + { + int shift = audio_sample_depth + 1 - 16; + int32_t value = ((const int32_t *)channel)[index]; + int32_t bias = shift > 0 ? (int32_t)1 << (shift - 1) : 0; + + return shift > 0 ? (value + bias) >> shift : value; + } +} + +static void audio_pcm_insert(const void *channel1, const void *channel2, + int count) +{ + int consumed = 0; + + while (consumed < count && audio_action != CODEC_ACTION_HALT) + { + int frames; + int written = 0; + int i; + + while (audio_paused && audio_action != CODEC_ACTION_HALT) + rb->sleep(1); + if (audio_action == CODEC_ACTION_HALT) + break; + + frames = count - consumed; + if (frames > AUDIO_PCM_FRAMES) + frames = AUDIO_PCM_FRAMES; + for (i = 0; i < frames; i++) + { + int source = consumed + i; + int32_t left; + int32_t right; + + if (audio_stereo_mode == STEREO_INTERLEAVED) + { + left = audio_sample_value(channel1, source * 2); + right = audio_sample_value(channel1, source * 2 + 1); + } + else + { + left = audio_sample_value(channel1, source); + right = audio_stereo_mode == STEREO_MONO ? left : + audio_sample_value(channel2, source); + } + audio_pcm[i * 2] = audio_clip16(left); + audio_pcm[i * 2 + 1] = audio_clip16(right); + } + while (written < frames && audio_action != CODEC_ACTION_HALT) + { + int amount; + + while (audio_paused && audio_action != CODEC_ACTION_HALT) + rb->sleep(1); + amount = video_pcm_write(audio_pcm + written * 2, + frames - written); + if (amount == 0) + { + rb->sleep(1); + continue; + } + written += amount; + } + consumed += frames; + if (!audio_is_ready && audio_sample_rate > 0 && + video_pcm_buffered_samples() >= + audio_sample_rate * AUDIO_START_BUFFER_MS / 1000) + audio_is_ready = true; + } +} + +static void audio_codec_api_init(void) +{ + rb->memset(&video_ci, 0, sizeof(video_ci)); + + video_ci.dsp = rb->dsp_get_config(CODEC_IDX_AUDIO); + video_ci.codec_get_buffer = audio_codec_get_buffer; + video_ci.pcmbuf_insert = audio_pcm_insert; + video_ci.set_elapsed = audio_set_elapsed; + video_ci.read_filebuf = audio_read_filebuf; + video_ci.request_buffer = audio_request_buffer; + video_ci.advance_buffer = audio_advance_buffer; + video_ci.seek_buffer = audio_seek_buffer; + video_ci.seek_complete = audio_seek_complete; + video_ci.set_offset = audio_set_offset; + video_ci.configure = audio_configure; + video_ci.get_command = audio_get_command; + video_ci.loop_track = audio_loop_track; + video_ci.strip_filesize = audio_strip_filesize; + +#if defined(ARM_NEED_DIV0) + video_ci.__div0 = rb->__div0; +#endif + video_ci.sleep = rb->sleep; + video_ci.yield = rb->yield; + +#if NUM_CORES > 1 + video_ci.create_thread = rb->create_thread; + video_ci.thread_thaw = rb->thread_thaw; + video_ci.thread_wait = rb->thread_wait; + video_ci.semaphore_init = rb->semaphore_init; + video_ci.semaphore_wait = rb->semaphore_wait; + video_ci.semaphore_release = rb->semaphore_release; +#endif + + video_ci.commit_dcache = rb->commit_dcache; + video_ci.commit_discard_dcache = rb->commit_discard_dcache; + video_ci.commit_discard_idcache = rb->commit_discard_idcache; + video_ci.strcpy = rb->strcpy; + video_ci.strlen = rb->strlen; + video_ci.strcmp = rb->strcmp; + video_ci.strcat = rb->strcat; + video_ci.memset = rb->memset; + video_ci.memcpy = rb->memcpy; + video_ci.memmove = rb->memmove; + video_ci.memcmp = rb->memcmp; + video_ci.memchr = rb->memchr; +#if defined(DEBUG) || defined(SIMULATOR) + video_ci.debugf = rb->debugf; +#endif +#ifdef ROCKBOX_HAS_LOGF + video_ci.logf = rb->logf; +#endif + video_ci.qsort = rb->qsort; + +#ifdef RB_PROFILE + video_ci.profile_thread = rb->profile_thread; + video_ci.profstop = rb->profstop; + video_ci.profile_func_enter = rb->profile_func_enter; + video_ci.profile_func_exit = rb->profile_func_exit; +#endif + +#ifdef HAVE_RECORDING + video_ci.round_value_to_list32 = rb->round_value_to_list32; +#endif + video_ci.panicf = rb->panicf; +} + +static void audio_codec_worker(void) +{ + int status; +#ifdef HAVE_PRIORITY_SCHEDULING + /* Match mpegplayer: core DSP/AAC yields often enough that the normal + * codec priority can be starved by the video render loop. */ + int old_priority = rb->thread_set_priority(rb->thread_self(), + PRIORITY_PLAYBACK - 4); +#endif + + status = rb->codec_load_file("aac", &video_ci); + if (status >= 0) + status = rb->codec_run_proc(); + rb->codec_close(); +#ifdef HAVE_PRIORITY_SCHEDULING + rb->thread_set_priority(rb->thread_self(), old_priority); +#endif + if (status < 0 && audio_action != CODEC_ACTION_HALT) + audio_has_error = true; + audio_is_ready = true; + audio_is_playing = false; +} + +int video_audio_init(const char *filepath, + const struct mp4v_demux_res *demux, + void *codec_workspace, size_t workspace_size, + void *pcm_buffer, size_t pcm_buffer_size) +{ + size_t required_workspace = video_audio_workspace_size(demux); + + if (audio_initialized || filepath == NULL || demux == NULL || + codec_workspace == NULL || pcm_buffer == NULL || + required_workspace == 0 || + workspace_size < required_workspace || + demux->audio_format != MAKEFOURCC('m', 'p', '4', 'a') || + demux->audio_sample_rate == 0 || demux->audio_num_samples == 0) + return -1; + + audio_fd = rb->open(filepath, O_RDONLY); + if (audio_fd < 0) + return -1; + audio_file_size = rb->filesize(audio_fd); + if (audio_file_size <= 0) + { + rb->close(audio_fd); + audio_fd = -1; + return -1; + } + + audio_codec_workspace = codec_workspace; + audio_codec_workspace_size = workspace_size; + audio_file_buffer_offset = 0; + audio_file_buffer_length = 0; + audio_sample_rate = demux->audio_sample_rate; + audio_sample_depth = 29; + audio_stereo_mode = STEREO_NONINTERLEAVED; + audio_action = CODEC_ACTION_NULL; + audio_seek_target_ms = 0; + audio_paused = false; + audio_is_ready = false; + audio_is_playing = false; + audio_has_error = false; + + rb->memset(&video_id3, 0, sizeof(video_id3)); + video_id3.codectype = AFMT_MP4_AAC; + video_id3.frequency = demux->audio_sample_rate; + video_id3.lead_trim = demux->audio_lead_trim; + + audio_codec_api_init(); + video_ci.filesize = audio_file_size; + video_ci.curpos = 0; + video_ci.id3 = &video_id3; + video_ci.audio_hid = -1; + if (!video_pcm_init(audio_sample_rate, pcm_buffer, pcm_buffer_size)) + { + rb->close(audio_fd); + audio_fd = -1; + audio_codec_workspace = NULL; + audio_codec_workspace_size = 0; + return -1; + } + audio_initialized = true; + return 0; +} + +void video_audio_play(void) +{ + if (!audio_initialized || audio_is_playing) + return; + audio_action = CODEC_ACTION_NULL; + audio_paused = false; + audio_is_playing = true; + rb->codec_thread_do_callback(audio_codec_worker, NULL); +} + +void video_audio_pause(void) +{ + audio_paused = true; +} + +void video_audio_resume(void) +{ + audio_paused = false; +} + +void video_audio_seek(uint32_t target_ms) +{ + if (!audio_initialized || !audio_is_playing) + return; + + audio_is_ready = false; + audio_seek_target_ms = target_ms; + video_pcm_flush(target_ms, audio_sample_rate); + audio_action = CODEC_ACTION_SEEK_TIME; +} + +void video_audio_stop(void) +{ + if (!audio_initialized) + return; + + audio_action = CODEC_ACTION_HALT; + audio_paused = false; + rb->codec_thread_do_callback(NULL, NULL); + video_pcm_stop(); + if (audio_fd >= 0) + { + rb->close(audio_fd); + audio_fd = -1; + } + audio_codec_workspace = NULL; + audio_codec_workspace_size = 0; + audio_initialized = false; + audio_is_playing = false; +} + +bool video_audio_ready(void) +{ + return audio_is_ready || audio_has_error; +} + +bool video_audio_is_active(void) +{ + return audio_is_playing; +} + +bool video_audio_failed(void) +{ + return audio_has_error; +} + +#endif /* HAVE_HW_H264 */ diff --git a/apps/plugins/h264_player/video_audio.h b/apps/plugins/h264_player/video_audio.h new file mode 100644 index 0000000000..69c229804a --- /dev/null +++ b/apps/plugins/h264_player/video_audio.h @@ -0,0 +1,60 @@ +/*************************************************************************** + * H.264 player audio decode service. + * + * Decodes AAC audio via Rockbox's aac.codec and feeds a PCM ring buffer. + * Uses the audio clock from video_pcm for A/V synchronization. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ +#ifndef VIDEO_AUDIO_H +#define VIDEO_AUDIO_H + +#include +#include +#include +#include "mp4_demux.h" + +/* Return enough codec heap to retain every AAC chunk offset. Apple M4V files + * interleave audio and video densely, so a sparse chunk map cannot decode the + * audio stream correctly. Returns 0 if the size cannot be represented. */ +size_t video_audio_workspace_size(const struct mp4v_demux_res *demux); + +/* Initialize audio decoder and thread. + * filepath: MP4 file path (opens its own fd for independent seeks) + * demux: parsed MP4 with audio track data (audio_format != 0) + * Returns 0 on success, -1 on error. */ +int video_audio_init(const char *filepath, + const struct mp4v_demux_res *demux, + void *codec_workspace, size_t workspace_size, + void *pcm_buffer, size_t pcm_buffer_size); + +/* Start audio playback (begins decoding and filling PCM buffer). */ +void video_audio_play(void); + +/* Pause audio decode (PCM buffer drains, then silence). */ +void video_audio_pause(void); + +/* Resume audio decode after pause. */ +void video_audio_resume(void); + +/* Seek audio to target time in milliseconds. Flushes PCM buffer. */ +void video_audio_seek(uint32_t target_ms); + +/* Stop audio thread and release resources. Blocks until thread exits. */ +void video_audio_stop(void); + +/* Check if audio thread has pre-filled enough PCM data to start. */ +bool video_audio_ready(void); + +/* Check if audio thread is actively decoding (false after EOF or error). */ +bool video_audio_is_active(void); + +/* True if the dynamically loaded AAC codec failed to load or decode. */ +bool video_audio_failed(void); + +#endif /* VIDEO_AUDIO_H */ diff --git a/apps/plugins/h264_player/video_pcm.c b/apps/plugins/h264_player/video_pcm.c new file mode 100644 index 0000000000..72874bc9c4 --- /dev/null +++ b/apps/plugins/h264_player/video_pcm.c @@ -0,0 +1,250 @@ +/*************************************************************************** + * PCM ring buffer for video player audio output. + * + * Simplified version of mpegplayer's pcm_output.c. + * Single-producer (audio decode thread), single-consumer (DMA ISR). + * Audio-master clock via monotonic sample counter. + * + * get_more() runs in DMA ISR context: no yield, sleep, mutex, or file I/O. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ +#include "plugin.h" + +#ifdef HAVE_HW_H264 + +#include "system.h" +#include "kernel.h" +#include "pcm.h" +#include "pcm_mixer.h" +#include "pcm_sampr.h" +#include "video_pcm.h" + +#include + +/* Silence clip for underrun (256 stereo samples = ~5.8ms at 44.1kHz) */ +#define SILENCE_SAMPLES 256 + +static int16_t *pcm_buffer; +static uint32_t pcm_buffer_frames; +static int16_t silence[SILENCE_SAMPLES * 2]; + +/* Read/write positions in stereo samples (wrap via modulo) */ +static volatile uint32_t pcm_read_pos; +static volatile uint32_t pcm_write_pos; + +/* Master clock: stereo samples consumed by DMA (uint32_t sufficient + * for >27 hours at 44.1kHz; avoids torn reads on 32-bit ARM) */ +static volatile uint32_t clock_samples; + +/* Clock base: absolute time (ms) at last flush/init */ +static uint32_t clock_base_ms; +static uint32_t clock_sample_rate; + +/* Flush guard: prevents write during flush (set by main, checked by audio) */ +static volatile bool flush_pending; +static struct mutex write_mutex; + +/* Saved mixer frequency for restore on stop */ +static unsigned int saved_sampr; + +static uint32_t pcm_used(void) +{ + return pcm_write_pos - pcm_read_pos; +} + +static uint32_t pcm_free(void) +{ + uint32_t used = pcm_used(); + + return used < pcm_buffer_frames ? pcm_buffer_frames - used : 0; +} + +/* DMA callback runs in ISR context. + * Pulls from ring buffer, advances clock. On underrun, plays silence. */ +static void video_pcm_get_more(const void **start, size_t *size) +{ + uint32_t avail = pcm_used(); + + if (avail > 0) + { + uint32_t rd = pcm_read_pos % pcm_buffer_frames; + uint32_t chunk = avail; + + /* Don't wrap around the buffer end */ + if (rd + chunk > pcm_buffer_frames) + chunk = pcm_buffer_frames - rd; + + /* Limit chunk to one mixer frame so the entire returned buffer + * is consumed before pcm_read_pos advances. Without this, the + * mixer stores our pointer and gradually reads over 8 callbacks + * while the write side sees the positions as "free" and + * overwrites them, causing audible fragment repetition. */ + if (chunk > MIX_FRAME_SAMPLES) + chunk = MIX_FRAME_SAMPLES; + + *start = &pcm_buffer[rd * 2]; + *size = chunk * 4; /* stereo 16-bit = 4 bytes per sample */ + pcm_read_pos += chunk; + clock_samples += chunk; + } + else + { + /* Underrun: play silence. Do NOT advance the clock; + * advancing would create a permanent A/V offset because the + * audio content hasn't actually been played yet. */ + *start = silence; + *size = sizeof(silence); + } +} + +static const struct mixer_play_cbs video_pcm_cbs = { + .get_more = video_pcm_get_more, +}; + +bool video_pcm_init(uint32_t sample_rate, void *buffer, size_t buffer_size) +{ + if (sample_rate == 0 || buffer == NULL || + buffer_size / (2 * sizeof(int16_t)) < MIX_FRAME_SAMPLES) + return false; + + /* Save current frequency for restore */ + saved_sampr = rb->mixer_get_frequency(); + pcm_buffer = buffer; + pcm_buffer_frames = buffer_size / (2 * sizeof(int16_t)); + + /* Reset buffer state */ + pcm_read_pos = 0; + pcm_write_pos = 0; + clock_samples = 0; + clock_base_ms = 0; + clock_sample_rate = sample_rate; + flush_pending = false; + rb->mutex_init(&write_mutex); + rb->memset(silence, 0, sizeof(silence)); + + /* Set sample rate BEFORE starting playback + * (mixer_set_frequency calls mixer_reset which stops all channels) */ +#if INPUT_SRC_CAPS != 0 + rb->audio_set_input_source(AUDIO_SRC_PLAYBACK, SRCF_PLAYBACK); + rb->audio_set_output_source(AUDIO_SRC_PLAYBACK); +#endif + rb->mixer_set_frequency(sample_rate); + + /* Start playback with DMA callback */ + rb->mixer_channel_set_amplitude(PCM_MIXER_CHAN_PLAYBACK, MIX_AMP_UNITY); + rb->mixer_channel_play_data(PCM_MIXER_CHAN_PLAYBACK, + &video_pcm_cbs, NULL, 0); + return true; +} + +int video_pcm_write(const int16_t *pcm, int stereo_samples) +{ + int written = 0; + + rb->mutex_lock(&write_mutex); + if (flush_pending) + { + rb->mutex_unlock(&write_mutex); + return 0; + } + + while (written < stereo_samples) + { + uint32_t space = pcm_free(); + if (space == 0 || flush_pending) + break; + + uint32_t wr = pcm_write_pos % pcm_buffer_frames; + uint32_t chunk = stereo_samples - written; + + if (chunk > space) + chunk = space; + + /* Don't wrap around the buffer end */ + if (wr + chunk > pcm_buffer_frames) + chunk = pcm_buffer_frames - wr; + + rb->memcpy(&pcm_buffer[wr * 2], &pcm[written * 2], chunk * 4); + pcm_write_pos += chunk; + written += chunk; + } + + rb->mutex_unlock(&write_mutex); + return written; +} + +uint32_t video_pcm_get_clock_ms(void) +{ + uint32_t samples = clock_samples; /* atomic 32-bit read on ARM */ + if (clock_sample_rate == 0) + return clock_base_ms; + return clock_base_ms + (uint32_t)((uint64_t)samples * 1000 + / clock_sample_rate); +} + +void video_pcm_flush(uint32_t base_ms, uint32_t sample_rate) +{ + /* Serialize against the codec producer before resetting its indexes. */ + rb->mutex_lock(&write_mutex); + flush_pending = true; + + /* Stop mixer channel to invalidate any stale buffer pointer, + * then reset state, then restart (mpegplayer pattern). */ + rb->mixer_channel_stop(PCM_MIXER_CHAN_PLAYBACK); + + pcm_read_pos = 0; + pcm_write_pos = 0; + clock_samples = 0; + clock_base_ms = base_ms; + clock_sample_rate = sample_rate; + + /* Restart mixer with fresh callback */ + rb->mixer_channel_set_amplitude(PCM_MIXER_CHAN_PLAYBACK, MIX_AMP_UNITY); + rb->mixer_channel_play_data(PCM_MIXER_CHAN_PLAYBACK, + &video_pcm_cbs, NULL, 0); + + flush_pending = false; + rb->mutex_unlock(&write_mutex); +} + +void video_pcm_pause(bool pause) +{ + rb->mixer_channel_play_pause(PCM_MIXER_CHAN_PLAYBACK, !pause); +} + +void video_pcm_stop(void) +{ + flush_pending = false; + + /* Serialize callback removal before plugin-owned memory is released. */ + rb->mixer_channel_stop(PCM_MIXER_CHAN_PLAYBACK); + + /* Restore original sample rate */ + if (saved_sampr != 0) + rb->mixer_set_frequency(saved_sampr); + pcm_buffer = NULL; + pcm_buffer_frames = 0; +} + +bool video_pcm_has_space(int stereo_samples) +{ + return pcm_free() >= (uint32_t)stereo_samples; +} + +bool video_pcm_empty(void) +{ + return pcm_used() == 0; +} + +uint32_t video_pcm_buffered_samples(void) +{ + return pcm_used(); +} + +#endif /* HAVE_HW_H264 */ diff --git a/apps/plugins/h264_player/video_pcm.h b/apps/plugins/h264_player/video_pcm.h new file mode 100644 index 0000000000..964a913597 --- /dev/null +++ b/apps/plugins/h264_player/video_pcm.h @@ -0,0 +1,52 @@ +/*************************************************************************** + * PCM ring buffer for H.264 player audio output. + * + * Simplified version of mpegplayer's pcm_output.c. + * Audio-master clock: DMA callback advances clock_samples monotonically. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ +#ifndef VIDEO_PCM_H +#define VIDEO_PCM_H + +#include +#include +#include + +/* Initialize PCM output at the given sample rate. + * Saves and changes mixer frequency. */ +bool video_pcm_init(uint32_t sample_rate, void *buffer, size_t buffer_size); + +/* Write interleaved stereo int16 PCM to the ring buffer. + * Returns number of stereo samples actually written (may be less if full). */ +int video_pcm_write(const int16_t *pcm, int stereo_samples); + +/* Get audio master clock in milliseconds. + * Returns base_ms + (samples_played * 1000 / sample_rate). */ +uint32_t video_pcm_get_clock_ms(void); + +/* Flush ring buffer and set clock base (for seek). + * After flush, get_clock returns base_ms + samples_played. */ +void video_pcm_flush(uint32_t base_ms, uint32_t sample_rate); + +/* Pause/resume PCM output. */ +void video_pcm_pause(bool pause); + +/* Stop PCM output and restore original mixer frequency. */ +void video_pcm_stop(void); + +/* Check if ring buffer has space for at least n stereo samples. */ +bool video_pcm_has_space(int stereo_samples); + +/* Check if ring buffer is empty (all data played). */ +bool video_pcm_empty(void); + +/* Return number of stereo samples currently buffered. */ +uint32_t video_pcm_buffered_samples(void); + +#endif /* VIDEO_PCM_H */ diff --git a/apps/plugins/h264_player/video_playback.c b/apps/plugins/h264_player/video_playback.c new file mode 100644 index 0000000000..b366079da7 --- /dev/null +++ b/apps/plugins/h264_player/video_playback.c @@ -0,0 +1,1132 @@ +/*************************************************************************** + * S5L8702 hardware H.264 player. + * + * This is deliberately a narrow production contract: non-fragmented MP4/M4V, + * the measured iTunes 9.2.1 Constrained Baseline syntax (Level 1.3 or 3.0, + * one or two advertised reference pictures and ordered slices), + * <= 640x480, <= 30 fps, with optional AAC-LC audio. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ +#include "plugin.h" + +#ifdef HAVE_HW_H264 + +#include "backlight.h" +#include "button.h" +#include "crc32.h" +#include "dir.h" +#include "file.h" +#include "font.h" +#include "kernel.h" +#include "lang.h" +#include "lcd.h" +#include "menu.h" +#include "misc.h" +#include "mp4_demux.h" +#include "pcmbuf.h" +#include "rbpaths.h" +#include "settings.h" +#include "sound.h" +#include "splash.h" +#include "system.h" +#include "video_audio.h" +#include "video_pcm.h" +#include "video_playback.h" +#include "hw_h264.h" + +#include +#include +#include + +#define VIDEO_MAX_WIDTH 640 +#define VIDEO_MAX_HEIGHT 480 +#define VIDEO_MAX_LEVEL 30 +#define VIDEO_MAX_FPS 30 +#define VIDEO_MAX_SAMPLES 400000u +#define VIDEO_MAX_AUDIO_SAMPLES 600000u +#define VIDEO_READ_BUFFER (512u * 1024u) +#define VIDEO_OUTPUT_BUFFER (LCD_WIDTH * LCD_HEIGHT * 3u / 2u) +#define VIDEO_PCM_BUFFER (256u * 1024u) +#define VIDEO_OSD_SHOW_TICKS (HZ * 4) +#define VIDEO_RESUME_MAGIC 0x48323634u +#define VIDEO_RESUME_FILE PLUGIN_APPS_DATA_DIR \ + "/h264-resume-%08lx.dat" + +enum video_input_action +{ + VIDEO_INPUT_NONE = 0, + VIDEO_INPUT_EXIT, + VIDEO_INPUT_USB, + VIDEO_INPUT_MENU, + VIDEO_INPUT_SEEK_BACK, + VIDEO_INPUT_SEEK_FORWARD, +}; + +struct video_resume_record +{ + uint32_t magic; + uint32_t path_crc; + int32_t frame; + int32_t total_frames; +}; + +struct video_pool +{ + uint8_t *cursor; + uint8_t *end; +}; + +struct video_timing +{ + uint32_t run; + uint32_t in_run; + uint64_t ticks; +}; + +static void *video_pool_take(struct video_pool *pool, size_t size, + size_t alignment) +{ + uintptr_t current; + uintptr_t aligned; + + if (pool == NULL || alignment == 0) + return NULL; + current = (uintptr_t)pool->cursor; + aligned = (current + alignment - 1) & ~(uintptr_t)(alignment - 1); + if (aligned < current || aligned > (uintptr_t)pool->end || + size > (size_t)(pool->end - (uint8_t *)aligned)) + return NULL; + pool->cursor = (uint8_t *)aligned + size; + return (void *)aligned; +} + +static bool video_tables_sane(const struct mp4v_demux_res *demux) +{ + uint64_t timed_samples = 0; + uint32_t i; + bool video_sane = + demux->num_samples > 0 && + demux->num_samples <= VIDEO_MAX_SAMPLES && + demux->num_stco > 0 && + demux->num_stco <= demux->num_samples && + demux->num_stsc > 0 && + demux->num_stsc <= demux->num_stco && + demux->num_stts > 0 && + demux->num_stss <= demux->num_samples && + demux->timescale > 0; + bool audio_sane = + (demux->audio_num_samples == 0 && + demux->audio_num_stco == 0 && demux->audio_num_stsc == 0) || + (demux->audio_num_samples > 0 && + demux->audio_num_samples <= VIDEO_MAX_AUDIO_SAMPLES && + demux->audio_num_stco > 0 && + demux->audio_num_stco <= demux->audio_num_samples && + demux->audio_num_stsc > 0 && + demux->audio_num_stsc <= demux->audio_num_stco); + + if (!video_sane || !audio_sane) + return false; + for (i = 0; i < demux->num_stts; i++) + { + if (demux->stts[i].sample_count == 0 || + demux->stts[i].sample_delta == 0 || + (uint64_t)demux->stts[i].sample_delta * VIDEO_MAX_FPS < + demux->timescale) + return false; + timed_samples += demux->stts[i].sample_count; + if (timed_samples > demux->num_samples) + return false; + } + return timed_samples == demux->num_samples; +} + +/* Validate the actual video mapping after the second, full table pass. The + * old independent stco/stsc caps rejected long files authored by iTunes, + * whose normal layout is one video sample per chunk and may therefore have + * hundreds of thousands of chunks. The MP4 relationships themselves give + * tighter bounds: every chunk contains samples and every stsc run starts at + * a real chunk. */ +static bool video_stsc_sane(const struct mp4v_demux_res *demux) +{ + uint64_t mapped_samples = 0; + uint32_t i; + + if (demux->stsc == NULL || demux->num_stsc == 0 || + demux->stsc_cap < demux->num_stsc || + demux->sample_sizes == NULL || + demux->sample_sizes_cap < demux->num_samples || + demux->chunk_offsets == NULL || + demux->chunk_offsets_cap < demux->num_stco || + (demux->num_stss > 0 && + (demux->sync_samples == NULL || + demux->sync_samples_cap < demux->num_stss))) + return false; + for (i = 0; i < demux->num_stsc; i++) + { + const struct mp4v_stsc_entry *entry = &demux->stsc[i]; + uint32_t next_chunk = i + 1 < demux->num_stsc ? + demux->stsc[i + 1].first_chunk : demux->num_stco + 1; + + if ((i == 0 && entry->first_chunk != 1) || + entry->first_chunk == 0 || entry->first_chunk > demux->num_stco || + next_chunk <= entry->first_chunk || + next_chunk > demux->num_stco + 1 || + entry->samples_per_chunk == 0 || entry->sample_desc_index == 0) + return false; + mapped_samples += + (uint64_t)(next_chunk - entry->first_chunk) * + entry->samples_per_chunk; + if (mapped_samples > demux->num_samples) + return false; + } + if (mapped_samples != demux->num_samples) + return false; + for (i = 0; i < demux->num_stss; i++) + { + if (demux->sync_samples[i] == 0 || + demux->sync_samples[i] > demux->num_samples || + (i > 0 && demux->sync_samples[i] <= + demux->sync_samples[i - 1])) + return false; + } + return true; +} + +static uint32_t video_pts_ms(const struct mp4v_demux_res *demux, + const struct video_timing *timing) +{ + if (demux->timescale == 0) + return 0; + return (uint32_t)(timing->ticks * 1000u / demux->timescale); +} + +static void video_timing_advance(const struct mp4v_demux_res *demux, + struct video_timing *timing) +{ + const struct mp4v_stts_entry *entry; + + if (timing->run >= demux->num_stts) + return; + entry = &demux->stts[timing->run]; + timing->ticks += entry->sample_delta; + timing->in_run++; + if (timing->in_run >= entry->sample_count) + { + timing->run++; + timing->in_run = 0; + } +} + +static uint32_t video_duration_ms(const struct mp4v_demux_res *demux) +{ + uint64_t ticks = 0; + uint32_t i; + + if (demux->timescale == 0) + return 0; + for (i = 0; i < demux->num_stts; i++) + ticks += (uint64_t)demux->stts[i].sample_count * + demux->stts[i].sample_delta; + return (uint32_t)(ticks * 1000u / demux->timescale); +} + +static void video_timing_for_sample(const struct mp4v_demux_res *demux, + uint32_t sample, + struct video_timing *timing) +{ + uint32_t remaining = sample; + + rb->memset(timing, 0, sizeof(*timing)); + while (timing->run < demux->num_stts) + { + const struct mp4v_stts_entry *entry = + &demux->stts[timing->run]; + + if (remaining < entry->sample_count) + { + timing->in_run = remaining; + timing->ticks += (uint64_t)remaining * entry->sample_delta; + return; + } + timing->ticks += (uint64_t)entry->sample_count * + entry->sample_delta; + remaining -= entry->sample_count; + timing->run++; + } +} + +static uint32_t video_sample_for_ms(const struct mp4v_demux_res *demux, + uint32_t target_ms, + struct video_timing *timing) +{ + uint64_t target_ticks; + uint64_t ticks = 0; + uint32_t sample = 0; + uint32_t run; + + if (demux->timescale == 0) + { + rb->memset(timing, 0, sizeof(*timing)); + return 0; + } + target_ticks = (uint64_t)target_ms * demux->timescale / 1000u; + for (run = 0; run < demux->num_stts; run++) + { + const struct mp4v_stts_entry *entry = &demux->stts[run]; + uint64_t run_ticks = (uint64_t)entry->sample_count * + entry->sample_delta; + + if (target_ticks < ticks + run_ticks) + { + uint32_t in_run = entry->sample_delta == 0 ? 0 : + (uint32_t)((target_ticks - ticks) / entry->sample_delta); + + if (in_run >= entry->sample_count) + in_run = entry->sample_count - 1; + sample += in_run; + break; + } + ticks += run_ticks; + sample += entry->sample_count; + } + if (sample >= demux->num_samples) + sample = demux->num_samples - 1; + while (sample > 0 && !mp4v_is_keyframe(demux, sample)) + sample--; + video_timing_for_sample(demux, sample, timing); + return sample; +} + +static void video_resume_filename(const char *path, char *filename, + size_t size, uint32_t *crc_out) +{ + uint32_t crc = rb->crc_32(path, rb->strlen(path), 0xffffffff); + + rb->snprintf(filename, size, VIDEO_RESUME_FILE, (unsigned long)crc); + if (crc_out != NULL) + *crc_out = crc; +} + +static uint32_t video_resume_load(const char *path, uint32_t total_samples) +{ + struct video_resume_record record; + char filename[MAX_PATH]; + uint32_t crc; + int fd; + + video_resume_filename(path, filename, sizeof(filename), &crc); + fd = rb->open(filename, O_RDONLY); + if (fd < 0) + return 0; + if (rb->read(fd, &record, sizeof(record)) != sizeof(record)) + { + rb->close(fd); + return 0; + } + rb->close(fd); + if (record.magic != VIDEO_RESUME_MAGIC || record.path_crc != crc || + record.total_frames != (int32_t)total_samples || record.frame <= 0 || + record.frame >= (int32_t)(total_samples * 95u / 100u)) + return 0; + return (uint32_t)record.frame; +} + +static void video_resume_save(const char *path, uint32_t sample, + uint32_t total_samples) +{ + struct video_resume_record record; + char filename[MAX_PATH]; + uint32_t crc; + int fd; + + video_resume_filename(path, filename, sizeof(filename), &crc); + if (sample == 0 || sample >= total_samples * 95u / 100u) + { + rb->remove(filename); + return; + } + rb->mkdir(PLUGIN_APPS_DATA_DIR); + record.magic = VIDEO_RESUME_MAGIC; + record.path_crc = crc; + record.frame = (int32_t)sample; + record.total_frames = (int32_t)total_samples; + fd = rb->open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (fd < 0) + return; + (void)rb->write(fd, &record, sizeof(record)); + rb->close(fd); +} + +static void video_resume_clear(const char *path) +{ + char filename[MAX_PATH]; + + video_resume_filename(path, filename, sizeof(filename), NULL); + rb->remove(filename); +} + +static void video_volume_change(int delta) +{ + int minimum = rb->sound_min(SOUND_VOLUME); + int maximum = rb->sound_max(SOUND_VOLUME); + int volume = rb->global_status->volume + delta; + + if (volume < minimum) + volume = minimum; + if (volume > maximum) + volume = maximum; + if (volume != rb->global_status->volume) + rb->sound_set(SOUND_VOLUME, volume); +} + +static void video_set_paused(bool pause, bool *paused, long *start_tick, + long *pause_started, bool have_audio, + bool *cpu_boosted) +{ + if (pause == *paused) + return; + if (pause) + { + *paused = true; + *pause_started = (*rb->current_tick); + if (have_audio) + { + video_audio_pause(); + video_pcm_pause(true); + } + if (*cpu_boosted) + { + rb->cpu_boost(false); + *cpu_boosted = false; + } + } + else + { + if (!*cpu_boosted) + { + rb->cpu_boost(true); + *cpu_boosted = true; + } + *paused = false; + *start_tick += (*rb->current_tick) - *pause_started; + if (have_audio) + { + video_audio_resume(); + video_pcm_pause(false); + } + } +} + +static enum video_input_action video_input(bool *paused, long *start_tick, + long *pause_started, + bool have_audio, + bool *cpu_boosted, + long *overlay_until, + bool *redraw) +{ + int action = rb->get_action(CONTEXT_WPS, TIMEOUT_NOBLOCK); + + *redraw = false; + if (action == ACTION_NONE) + return VIDEO_INPUT_NONE; + if (action == ACTION_WPS_MENU) + return VIDEO_INPUT_MENU; + if (action == ACTION_WPS_STOP) + return VIDEO_INPUT_EXIT; + if (action == ACTION_WPS_VOLUP) + { + video_volume_change(1); + *overlay_until = (*rb->current_tick) + VIDEO_OSD_SHOW_TICKS; + *redraw = true; + return VIDEO_INPUT_NONE; + } + if (action == ACTION_WPS_VOLDOWN) + { + video_volume_change(-1); + *overlay_until = (*rb->current_tick) + VIDEO_OSD_SHOW_TICKS; + *redraw = true; + return VIDEO_INPUT_NONE; + } + if (action == ACTION_WPS_SKIPPREV || action == ACTION_WPS_SEEKBACK) + return VIDEO_INPUT_SEEK_BACK; + if (action == ACTION_WPS_SKIPNEXT || action == ACTION_WPS_SEEKFWD) + return VIDEO_INPUT_SEEK_FORWARD; + if (action == ACTION_WPS_PLAY) + { + video_set_paused(!*paused, paused, start_tick, pause_started, + have_audio, cpu_boosted); + *overlay_until = (*rb->current_tick) + VIDEO_OSD_SHOW_TICKS; + *redraw = true; + } + if (rb->default_event_handler(action) == SYS_USB_CONNECTED) + return VIDEO_INPUT_USB; + return VIDEO_INPUT_NONE; +} + +enum video_menu_item +{ + VIDEO_MENU_RESUME = 0, + VIDEO_MENU_QUIT, +}; + +static enum video_input_action video_show_menu( + bool *paused, long *start_tick, long *pause_started, bool have_audio, + bool *cpu_boosted, long *overlay_until, bool *redraw) +{ + bool resume_after = !*paused; + int selection; + + MENUITEM_STRINGLIST(menu, "H.264 Player", NULL, + ID2P(LANG_RESUME_PLAYBACK), + ID2P(LANG_MENU_QUIT)); + + video_set_paused(true, paused, start_tick, pause_started, have_audio, + cpu_boosted); + rb->button_clear_queue(); + selection = rb->do_menu(&menu, NULL, NULL, false); + rb->button_clear_queue(); + if (selection == MENU_ATTACHED_USB) + return VIDEO_INPUT_USB; + if (selection == VIDEO_MENU_QUIT) + return VIDEO_INPUT_EXIT; + if (resume_after) + video_set_paused(false, paused, start_tick, pause_started, + have_audio, cpu_boosted); + *overlay_until = (*rb->current_tick) + VIDEO_OSD_SHOW_TICKS; + *redraw = true; + return VIDEO_INPUT_NONE; +} + +static void video_scale_plane(uint8_t *destination, int destination_stride, + int destination_width, int destination_height, + const uint8_t *source, int source_stride, + int source_width, int source_height) +{ + uint32_t source_y = 0; + uint32_t step_y; + uint32_t step_x; + int row; + + if (destination_width <= 0 || destination_height <= 0 || + source_width <= 0 || source_height <= 0) + return; + step_x = ((uint32_t)source_width << 16) / destination_width; + step_y = ((uint32_t)source_height << 16) / destination_height; + for (row = 0; row < destination_height; row++) + { + const uint8_t *source_row = source + + (source_y >> 16) * source_stride; + uint8_t *destination_row = destination + + row * destination_stride; + uint32_t source_x = 0; + int column; + + for (column = 0; column < destination_width; column++) + { + destination_row[column] = source_row[source_x >> 16]; + source_x += step_x; + } + source_y += step_y; + } +} + +static uint8_t video_clamp_yuv(int value) +{ + if (value < 0) + return 0; + if (value > 255) + return 255; + return (uint8_t)value; +} + +static void video_yuv_color(int red, int green, int blue, + uint8_t *y, uint8_t *u, uint8_t *v) +{ + *y = video_clamp_yuv( + ((66 * red + 129 * green + 25 * blue + 128) >> 8) + 16); + *u = video_clamp_yuv( + ((-38 * red - 74 * green + 112 * blue + 128) >> 8) + 128); + *v = video_clamp_yuv( + ((112 * red - 94 * green - 18 * blue + 128) >> 8) + 128); +} + +static void video_yuv_pixel(uint8_t * const *planes, int x, int y, + int red, int green, int blue) +{ + uint8_t py; + uint8_t pu; + uint8_t pv; + + if (x < 0 || x >= LCD_WIDTH || y < 0 || y >= LCD_HEIGHT) + return; + video_yuv_color(red, green, blue, &py, &pu, &pv); + planes[0][y * LCD_WIDTH + x] = py; + planes[1][(y / 2) * (LCD_WIDTH / 2) + x / 2] = pu; + planes[2][(y / 2) * (LCD_WIDTH / 2) + x / 2] = pv; +} + +static void video_yuv_rect(uint8_t * const *planes, int x, int y, + int width, int height, + int red, int green, int blue) +{ + int row; + int column; + + for (row = MAX(0, y); row < MIN(LCD_HEIGHT, y + height); row++) + for (column = MAX(0, x); + column < MIN(LCD_WIDTH, x + width); column++) + video_yuv_pixel(planes, column, row, red, green, blue); +} + +static int video_yuv_text_width(const char *text) +{ + struct font *font = rb->font_get(FONT_SYSFIXED); + int width = 0; + + if (font == NULL) + return 0; + while (*text != '\0') + width += rb->font_get_width(font, (unsigned char)*text++); + return width; +} + +static void video_yuv_text(uint8_t * const *planes, int x, int y, + const char *text, + int red, int green, int blue) +{ + struct font *font = rb->font_get(FONT_SYSFIXED); + + if (font == NULL || font->depth != 0) + return; + while (*text != '\0' && x < LCD_WIDTH) + { + unsigned char ch = *text++; + int glyph_width = rb->font_get_width(font, ch); + const unsigned char *bits = rb->font_get_bits(font, ch); + int column; + + for (column = 0; column < glyph_width; column++) + { + const unsigned char *source = bits + column; + int row; + + for (row = 0; row < (int)font->height; row++) + if (source[(row >> 3) * glyph_width] & + (1u << (row & 7))) + video_yuv_pixel(planes, x + column, y + row, + red, green, blue); + } + x += glyph_width; + } +} + +static void video_time_text(uint32_t milliseconds, char *text, size_t size) +{ + uint32_t seconds = milliseconds / 1000u; + + if (seconds >= 3600u) + rb->snprintf(text, size, "%lu:%02lu:%02lu", + (unsigned long)(seconds / 3600u), + (unsigned long)((seconds / 60u) % 60u), + (unsigned long)(seconds % 60u)); + else + rb->snprintf(text, size, "%lu:%02lu", + (unsigned long)(seconds / 60u), + (unsigned long)(seconds % 60u)); +} + +static void video_draw_status_icon(uint8_t * const *planes, int x, int y, + bool paused) +{ + int row; + + if (paused) + { + video_yuv_rect(planes, x, y, 2, 9, 255, 255, 255); + video_yuv_rect(planes, x + 5, y, 2, 9, 255, 255, 255); + return; + } + for (row = 0; row < 9; row++) + { + int width = row <= 4 ? row + 1 : 9 - row; + + video_yuv_rect(planes, x, y + row, width, 1, 255, 255, 255); + } +} + +static void video_draw_overlay(uint8_t * const *planes, bool paused, + uint32_t position_ms, uint32_t duration_ms, + bool visible) +{ + char current[20]; + char duration[20]; + char volume[20]; + char minimum_volume[20]; + const char *unit = rb->sound_unit(SOUND_VOLUME); + int top = LCD_HEIGHT - 34; + int bar_x = 2; + int bar_y = top + 21; + int bar_height = 8; + int volume_right = LCD_WIDTH - 2; + int volume_field_width; + int volume_width; + int space_width; + int bar_width; + int fill_width; + int duration_width; + + if (!visible) + return; + + video_time_text(position_ms, current, sizeof(current)); + video_time_text(duration_ms, duration, sizeof(duration)); + rb->snprintf(volume, sizeof(volume), "%d%s", + rb->sound_val2phys(SOUND_VOLUME, rb->global_status->volume), unit); + rb->snprintf(minimum_volume, sizeof(minimum_volume), "%d%s", + rb->sound_val2phys(SOUND_VOLUME, + rb->sound_min(SOUND_VOLUME)), unit); + volume_field_width = video_yuv_text_width(minimum_volume); + volume_width = video_yuv_text_width(volume); + space_width = video_yuv_text_width(" "); + bar_width = volume_right - volume_field_width - space_width - bar_x; + fill_width = duration_ms == 0 ? 0 : + (int)((uint64_t)MIN(position_ms, duration_ms) * + (bar_width - 2) / duration_ms); + duration_width = video_yuv_text_width(duration); + + /* Match MPEG player's raised purple OSD and element placement. */ + video_yuv_rect(planes, 0, top, LCD_WIDTH, 34, 115, 117, 189); + video_yuv_rect(planes, 0, top, LCD_WIDTH, 1, 220, 221, 239); + video_yuv_rect(planes, 0, top + 1, LCD_WIDTH, 1, 159, 160, 210); + video_yuv_rect(planes, 0, LCD_HEIGHT - 2, LCD_WIDTH, 1, 93, 95, 153); + video_yuv_rect(planes, 0, LCD_HEIGHT - 1, LCD_WIDTH, 1, 57, 58, 94); + video_yuv_text(planes, bar_x, top + 6, current, 255, 255, 255); + video_yuv_text(planes, bar_x + bar_width - duration_width, top + 6, + duration, 255, 255, 255); + video_draw_status_icon(planes, + bar_x + (bar_width - 7) / 2, + top + 5, paused); + video_yuv_rect(planes, bar_x, bar_y, bar_width, bar_height, + 255, 255, 255); + video_yuv_rect(planes, bar_x + 1, bar_y + 1, + bar_width - 2, bar_height - 2, 0, 0, 0); + video_yuv_rect(planes, bar_x + 1, bar_y + 1, + fill_width, bar_height - 2, 255, 255, 255); + video_yuv_text(planes, volume_right - volume_width, bar_y, + volume, 255, 255, 255); +} + +static void video_draw_frame(const uint8_t *y, const uint8_t *cb, + const uint8_t *cr, int width, int height, + int stride, uint8_t *scaled, bool paused, + uint32_t position_ms, uint32_t duration_ms, + bool overlay_visible) +{ + unsigned char *planes[3]; + int draw_width; + int draw_height; + int x; + int y_pos; + + if (y == NULL || cb == NULL || cr == NULL || scaled == NULL || + width <= 0 || height <= 0 || stride < width) + return; + + draw_width = LCD_WIDTH; + draw_height = (int)((int64_t)height * LCD_WIDTH / width); + if (draw_height > LCD_HEIGHT) + { + draw_height = LCD_HEIGHT; + draw_width = (int)((int64_t)width * LCD_HEIGHT / height); + } + draw_width &= ~1; + draw_height &= ~1; + x = ((LCD_WIDTH - draw_width) / 2) & ~1; + y_pos = ((LCD_HEIGHT - draw_height) / 2) & ~1; + + planes[0] = scaled; + planes[1] = scaled + LCD_WIDTH * LCD_HEIGHT; + planes[2] = planes[1] + LCD_WIDTH * LCD_HEIGHT / 4; + rb->memset(planes[0], 16, LCD_WIDTH * LCD_HEIGHT); + rb->memset(planes[1], 128, LCD_WIDTH * LCD_HEIGHT / 4); + rb->memset(planes[2], 128, LCD_WIDTH * LCD_HEIGHT / 4); + video_scale_plane(planes[0] + y_pos * LCD_WIDTH + x, LCD_WIDTH, + draw_width, draw_height, y, stride, width, height); + video_scale_plane(planes[1] + (y_pos / 2) * (LCD_WIDTH / 2) + x / 2, + LCD_WIDTH / 2, draw_width / 2, draw_height / 2, + cb, stride / 2, width / 2, height / 2); + video_scale_plane(planes[2] + (y_pos / 2) * (LCD_WIDTH / 2) + x / 2, + LCD_WIDTH / 2, draw_width / 2, draw_height / 2, + cr, stride / 2, width / 2, height / 2); + video_draw_overlay(planes, paused, position_ms, duration_ms, + overlay_visible); + rb->lcd_blit_yuv(planes, 0, 0, LCD_WIDTH, 0, 0, + LCD_WIDTH, LCD_HEIGHT); +} + +static void video_redraw_frame(struct vpu_h264 *decoder, uint8_t *scaled, + bool paused, uint32_t position_ms, + uint32_t duration_ms, bool overlay_visible) +{ + const uint8_t *frame_y; + const uint8_t *frame_cb; + const uint8_t *frame_cr; + int width; + int height; + int stride; + + rb->hw_h264->get_frame(decoder, &frame_y, &frame_cb, &frame_cr, + &width, &height, &stride); + video_draw_frame(frame_y, frame_cb, frame_cr, width, height, stride, + scaled, paused, position_ms, duration_ms, + overlay_visible); +} + +int video_h264_play(const char *filepath, void *buffer, size_t buffer_size) +{ + static struct mp4v_demux_res demux; + static uint32_t probe_video_sample[1]; + static uint32_t probe_video_chunk[1]; + static uint32_t probe_video_sync[1]; + struct video_pool pool; + struct video_timing timing; + struct vpu_h264 *decoder = NULL; + struct mp4v_stsc_entry *video_stsc; + uint32_t *video_samples; + uint32_t *video_chunks; + uint32_t *video_sync; + uint8_t *decoder_buffer; + uint8_t *read_buffer; + uint8_t *scale_buffer; + void *audio_codec_workspace; + void *pcm_buffer; + size_t decoder_size; + size_t audio_codec_workspace_size; + int video_fd = -1; + uint32_t duration_ms; + uint32_t displayed_ms = 0; + uint32_t sample = 0; + uint32_t last_sample = 0; + long start_tick; + long pause_started = 0; + long overlay_until; + bool paused = false; + bool cpu_boosted = false; + bool have_audio = false; + bool have_frame = false; + bool overlay_drawn = false; + bool audio_master = false; + enum video_input_action action = VIDEO_INPUT_NONE; + int result = -1; + + if (filepath == NULL || filepath[0] == '\0' || buffer == NULL || + buffer_size == 0) + return -1; + + rb->splash(0, "Loading video..."); + rb->cpu_boost(true); + cpu_boosted = true; + rb->memset(&demux, 0, sizeof(demux)); + if (mp4v_demux_open(filepath, &demux, + probe_video_sample, 1, + probe_video_chunk, 1, NULL, 0, + probe_video_sync, 1) < 0) + { + rb->splash(HZ * 2, "Invalid MP4/M4V file"); + goto cleanup; + } + if (demux.format != MAKEFOURCC('a', 'v', 'c', '1') || + demux.avc_profile != 66 || demux.avc_level > VIDEO_MAX_LEVEL || + demux.width == 0 || demux.height == 0 || + demux.width > VIDEO_MAX_WIDTH || demux.height > VIDEO_MAX_HEIGHT || + demux.nalu_len_size < 1 || demux.nalu_len_size > 4) + { + rb->splash(HZ * 3, "Need H.264 Baseline <= L3.0\nMax 640x480"); + goto cleanup; + } + if (!video_tables_sane(&demux)) + { + rb->splash(HZ * 3, "Movie tables exceed safe limit"); + goto cleanup; + } + + pool.cursor = buffer; + pool.end = (uint8_t *)buffer + buffer_size; + video_samples = video_pool_take( + &pool, demux.num_samples * sizeof(*video_samples), 32); + video_chunks = video_pool_take( + &pool, demux.num_stco * sizeof(*video_chunks), 32); + video_stsc = video_pool_take( + &pool, demux.num_stsc * sizeof(*video_stsc), 32); + video_sync = demux.num_stss > 0 ? video_pool_take( + &pool, demux.num_stss * sizeof(*video_sync), 32) : NULL; + decoder_size = rb->hw_h264->buf_size( + (demux.width + 15) & ~15, (demux.height + 15) & ~15); + decoder_buffer = video_pool_take(&pool, decoder_size, 4096); + read_buffer = video_pool_take(&pool, VIDEO_READ_BUFFER, 32); + scale_buffer = video_pool_take(&pool, VIDEO_OUTPUT_BUFFER, 32); + audio_codec_workspace_size = demux.audio_num_samples > 0 ? + video_audio_workspace_size(&demux) : 0; + audio_codec_workspace = audio_codec_workspace_size > 0 ? + video_pool_take(&pool, audio_codec_workspace_size, + CACHEALIGN_SIZE) : NULL; + pcm_buffer = audio_codec_workspace_size > 0 ? + video_pool_take(&pool, VIDEO_PCM_BUFFER, CACHEALIGN_SIZE) : NULL; + if (video_samples == NULL || video_chunks == NULL || + video_stsc == NULL || + (demux.num_stss > 0 && video_sync == NULL) || + decoder_buffer == NULL || read_buffer == NULL || + scale_buffer == NULL || + (demux.audio_num_samples > 0 && + (audio_codec_workspace == NULL || pcm_buffer == NULL))) + { + rb->splash(HZ * 2, "Not enough video memory"); + goto cleanup; + } + rb->splash(0, "Loading movie index..."); + if (mp4v_demux_open(filepath, &demux, + video_samples, demux.num_samples, + video_chunks, demux.num_stco, + video_stsc, demux.num_stsc, + video_sync, demux.num_stss) < 0 || + !video_tables_sane(&demux) || !video_stsc_sane(&demux)) + { + rb->splash(HZ * 2, "MP4 table parse failed"); + goto cleanup; + } + duration_ms = video_duration_ms(&demux); + sample = video_resume_load(filepath, demux.num_samples); + while (sample > 0 && !mp4v_is_keyframe(&demux, sample)) + sample--; + video_timing_for_sample(&demux, sample, &timing); + + /* Preparation, decode, and LCD presentation need the 108 MHz bus clock. + * Keep one boost reference from table parsing through active playback. */ + rb->splash(0, "Starting video..."); + decoder = rb->hw_h264->open( + decoder_buffer, decoder_size, + (demux.width + 15) & ~15, (demux.height + 15) & ~15); + if (decoder == NULL || + rb->hw_h264->configure(decoder, demux.codecdata, + demux.codecdata_len) < 0) + { + rb->splash(HZ * 2, "H.264 decoder init failed"); + goto cleanup; + } + video_fd = rb->open(filepath, O_RDONLY); + if (video_fd < 0) + goto cleanup; + + rb->pcmbuf_fade(false, true); + rb->sound_set(SOUND_VOLUME, rb->global_status->volume); + start_tick = (*rb->current_tick) - + (long)((uint64_t)video_pts_ms(&demux, &timing) * HZ / 1000u); + overlay_until = (*rb->current_tick) + VIDEO_OSD_SHOW_TICKS; + if (demux.audio_format == MAKEFOURCC('m', 'p', '4', 'a') && + demux.audio_codecdata_len > 0 && + video_audio_init(filepath, &demux, audio_codec_workspace, + audio_codec_workspace_size, pcm_buffer, + VIDEO_PCM_BUFFER) == 0) + { + int wait = 0; + uint32_t initial_ms = video_pts_ms(&demux, &timing); + + have_audio = true; + audio_master = true; + video_audio_play(); + if (initial_ms > 0) + video_audio_seek(initial_ms); + while (!video_audio_ready() && wait < HZ && action == VIDEO_INPUT_NONE) + { + bool redraw_requested; + + action = video_input(&paused, &start_tick, + &pause_started, true, &cpu_boosted, + &overlay_until, &redraw_requested); + if (action == VIDEO_INPUT_MENU) + action = video_show_menu( + &paused, &start_tick, &pause_started, true, + &cpu_boosted, &overlay_until, &redraw_requested); + rb->sleep(1); + wait++; + } + if (video_audio_failed()) + { + video_audio_stop(); + have_audio = false; + audio_master = false; + } + } + + rb->lcd_set_foreground(LCD_BLACK); + rb->lcd_clear_display(); + rb->lcd_update(); + rb->backlight_on(); + rb->backlight_set_timeout(0); + while (sample < demux.num_samples && action == VIDEO_INPUT_NONE) + { + uint32_t offset; + uint32_t size; + uint32_t pts_ms = video_pts_ms(&demux, &timing); + bool seek_requested = false; + int decoded; + + while (action == VIDEO_INPUT_NONE) + { + uint32_t clock_ms; + bool redraw_requested; + + action = video_input(&paused, &start_tick, + &pause_started, have_audio, &cpu_boosted, + &overlay_until, &redraw_requested); + if (action == VIDEO_INPUT_MENU) + action = video_show_menu( + &paused, &start_tick, &pause_started, have_audio, + &cpu_boosted, &overlay_until, &redraw_requested); + if (redraw_requested && have_frame) + { + video_redraw_frame(decoder, scale_buffer, paused, + displayed_ms, duration_ms, true); + overlay_drawn = true; + } + if (action == VIDEO_INPUT_SEEK_BACK || + action == VIDEO_INPUT_SEEK_FORWARD) + { + int64_t target = + (int64_t)(audio_master ? video_pcm_get_clock_ms() : + pts_ms) + + (action == VIDEO_INPUT_SEEK_BACK ? -10000 : 10000); + + if (target < 0) + target = 0; + if ((uint64_t)target >= duration_ms && duration_ms > 0) + target = duration_ms - 1; + sample = video_sample_for_ms(&demux, (uint32_t)target, + &timing); + pts_ms = video_pts_ms(&demux, &timing); + rb->hw_h264->close(decoder); + decoder = rb->hw_h264->open( + decoder_buffer, decoder_size, + (demux.width + 15) & ~15, + (demux.height + 15) & ~15); + if (decoder == NULL || + rb->hw_h264->configure(decoder, demux.codecdata, + demux.codecdata_len) < 0) + { + rb->splash(HZ * 2, "H.264 seek failed"); + decoder = NULL; + goto cleanup; + } + if (have_audio) + video_audio_seek(pts_ms); + start_tick = (*rb->current_tick) - + (long)((uint64_t)pts_ms * HZ / 1000u); + overlay_until = (*rb->current_tick) + VIDEO_OSD_SHOW_TICKS; + action = VIDEO_INPUT_NONE; + seek_requested = true; + break; + } + if (action != VIDEO_INPUT_NONE) + break; + if (paused) + { + if (overlay_drawn && + !TIME_BEFORE((*rb->current_tick), overlay_until)) + { + video_redraw_frame(decoder, scale_buffer, true, + displayed_ms, duration_ms, false); + overlay_drawn = false; + } + rb->sleep(1); + continue; + } + clock_ms = audio_master ? video_pcm_get_clock_ms() : + (uint32_t)(((*rb->current_tick) - start_tick) * 1000 / HZ); + if (audio_master && + (video_audio_failed() || + (!video_audio_is_active() && video_pcm_empty()))) + { + video_audio_stop(); + have_audio = false; + audio_master = false; + start_tick = (*rb->current_tick) - + (long)((uint64_t)clock_ms * HZ / 1000u); + } + if (clock_ms + 2 >= pts_ms) + break; + rb->sleep(1); + } + if (action != VIDEO_INPUT_NONE) + break; + if (seek_requested) + continue; + if (mp4v_get_sample_offset(&demux, sample, &offset, &size) < 0 || + size == 0 || size > VIDEO_READ_BUFFER || + rb->lseek(video_fd, offset, SEEK_SET) < 0 || + rb->read(video_fd, read_buffer, size) != (ssize_t)size) + { + rb->splash(HZ * 2, "Video sample read failed"); + goto cleanup; + } + decoded = rb->hw_h264->decode_sample( + decoder, read_buffer, size, demux.nalu_len_size); + if (decoded < 0) + { + rb->splash(HZ * 2, "VPU sample decode failed"); + goto cleanup; + } + if (decoded > 0) + { + const uint8_t *frame_y; + const uint8_t *frame_cb; + const uint8_t *frame_cr; + int width; + int height; + int stride; + bool overlay_visible = + TIME_BEFORE((*rb->current_tick), overlay_until); + + rb->hw_h264->get_frame(decoder, &frame_y, &frame_cb, &frame_cr, + &width, &height, &stride); + video_draw_frame(frame_y, frame_cb, frame_cr, width, height, + stride, scale_buffer, paused, pts_ms, + duration_ms, overlay_visible); + displayed_ms = pts_ms; + have_frame = true; + overlay_drawn = overlay_visible; + } + last_sample = sample; + video_timing_advance(&demux, &timing); + sample++; + } + result = action == VIDEO_INPUT_USB ? 2 : + (action == VIDEO_INPUT_EXIT ? 1 : 0); + if (result == 0) + video_resume_clear(filepath); + else + video_resume_save(filepath, last_sample, demux.num_samples); + +cleanup: + if (have_audio) + video_audio_stop(); + if (video_fd >= 0) + rb->close(video_fd); + if (decoder != NULL) + rb->hw_h264->close(decoder); + rb->pcmbuf_fade(false, false); + rb->backlight_set_timeout(rb->global_settings->backlight_timeout); + rb->lcd_set_foreground(LCD_BLACK); + rb->lcd_clear_display(); + rb->lcd_update(); + if (cpu_boosted) + rb->cpu_boost(false); + return result; +} + +#endif /* HAVE_HW_H264 */ diff --git a/apps/plugins/h264_player/video_playback.h b/apps/plugins/h264_player/video_playback.h new file mode 100644 index 0000000000..2433c44396 --- /dev/null +++ b/apps/plugins/h264_player/video_playback.h @@ -0,0 +1,21 @@ +/*************************************************************************** + * Hardware H.264 player implementation. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ +#ifndef VIDEO_PLAYBACK_H +#define VIDEO_PLAYBACK_H + +#include + +/* Play a validated H.264 Baseline/AAC-LC MP4 from caller-owned memory. + * The caller must retain the plugin audio buffer until this function returns. + * Returns 0 at end of stream, 1 on user exit, 2 for USB, or -1 on failure. */ +int video_h264_play(const char *filepath, void *buffer, size_t buffer_size); + +#endif /* VIDEO_PLAYBACK_H */ diff --git a/apps/plugins/viewers.config b/apps/plugins/viewers.config index d745a29cfe..151b97ad7c 100644 --- a/apps/plugins/viewers.config +++ b/apps/plugins/viewers.config @@ -76,6 +76,11 @@ wav,viewers/wavview,10 wav,viewers/wav2wv,- wav,viewers/mp3_encoder,- wav,viewers/test_codec,- +#ifdef HAVE_HW_H264 +mp4,viewers/h264_player,4 +m4v,viewers/h264_player,4 +mov,viewers/h264_player,4 +#endif mpg,viewers/mpegplayer,4 mpeg,viewers/mpegplayer,4 mpv,viewers/mpegplayer,4 diff --git a/docs/CREDITS b/docs/CREDITS index edb859249c..24b0feb054 100644 --- a/docs/CREDITS +++ b/docs/CREDITS @@ -767,6 +767,7 @@ Michael McAllister Hemant Kumar Jerry Shen Olivier Senn +David Cormier The libmad team The wavpack team diff --git a/firmware/SOURCES b/firmware/SOURCES index 8b524edcc4..02a9d7cd83 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1680,6 +1680,10 @@ target/arm/s5l8702/ipod6g/adc-6g.c target/arm/s5l8702/ipod6g/powermgmt-6g.c target/arm/s5l8702/ipod6g/power-6g.c target/arm/s5l8702/ipod6g/storage_ata-6g.c +#if defined(HAVE_HW_H264) && !defined(BOOTLOADER) +target/arm/s5l8702/ipod6g/vpu-6g.c +target/arm/s5l8702/ipod6g/vpu-h264-6g.c +#endif #if (CONFIG_RTC == RTC_NANO2G) target/arm/s5l8702/ipod6g/rtc-6g.c #endif diff --git a/firmware/export/config/ipod6g.h b/firmware/export/config/ipod6g.h index a3ade43de5..c25192526f 100644 --- a/firmware/export/config/ipod6g.h +++ b/firmware/export/config/ipod6g.h @@ -58,6 +58,11 @@ /* define this to enable JPEG decoding */ #define HAVE_JPEG +/* S5L8702 VPU-B hardware H.264 decoder. */ +#ifndef SIMULATOR +#define HAVE_HW_H264 +#endif + /* define this if you can invert the colours on your LCD */ //#define HAVE_LCD_INVERT diff --git a/firmware/export/hw_h264.h b/firmware/export/hw_h264.h new file mode 100644 index 0000000000..d7a403284f --- /dev/null +++ b/firmware/export/hw_h264.h @@ -0,0 +1,39 @@ +/*************************************************************************** + * Hardware H.264 decoder interface exposed to viewer plugins. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ + +#ifndef HW_H264_H +#define HW_H264_H + +#include +#include + +struct vpu_h264; + +/* The container, audio, synchronization, and UI layers live in the plugin. + * Only the target hardware decoder is exported by core. */ +struct hw_h264_api +{ + size_t (*buf_size)(int max_w, int max_h); + struct vpu_h264 *(*open)(void *buf, size_t buf_size, + int max_w, int max_h); + int (*configure)(struct vpu_h264 *decoder, + const uint8_t *avcc, int avcc_len); + int (*decode_sample)(struct vpu_h264 *decoder, const uint8_t *sample, + int sample_len, int nalu_len_size); + void (*get_frame)(const struct vpu_h264 *decoder, + const uint8_t **y, const uint8_t **cb, + const uint8_t **cr, int *w, int *h, int *stride); + void (*close)(struct vpu_h264 *decoder); +}; + +extern const struct hw_h264_api target_hw_h264_api; + +#endif /* HW_H264_H */ diff --git a/firmware/export/s5l87xx.h b/firmware/export/s5l87xx.h index feb220286a..0ece79c0a6 100644 --- a/firmware/export/s5l87xx.h +++ b/firmware/export/s5l87xx.h @@ -1643,6 +1643,28 @@ The following peripherals are not present in the Samsung S5L8700 datasheet. Information for them was gathered solely by reverse-engineering Apple's firmware. */ +/* VPU-B H.264 decoder - S5L8702 */ +#if CONFIG_CPU == S5L8702 +#define VPU_MODE (*((REG32_PTR_T)(0x38100314))) +#define VPU_BASE 0x39800000 +#define VPU_REG(off) (*((REG32_PTR_T)(VPU_BASE + (off)))) + +#define VPU_DPB_Y(i) VPU_REG((i) * 12) +#define VPU_DPB_CB(i) VPU_REG((i) * 12 + 4) +#define VPU_DPB_CR(i) VPU_REG((i) * 12 + 8) +#define VPU_OUT_Y VPU_REG(0x0cc) +#define VPU_OUT_CB VPU_REG(0x0d0) +#define VPU_OUT_CR VPU_REG(0x0d4) +#define VPU_CTRL_BUF VPU_REG(0x0d8) +#define VPU_SLICE_DESC VPU_REG(0x0dc) +#define VPU_DIMS VPU_REG(0x0e0) +#define VPU_STRIDES VPU_REG(0x0e4) +#define VPU_CTRL VPU_REG(0x0e8) +#define VPU_STATUS0 VPU_REG(0x0f0) +#define VPU_STATUS1 VPU_REG(0x0f4) +#define VPU_CONFIG VPU_REG(0x118) +#endif + /* Hardware AES crypto unit - S5L8701+ */ #if CONFIG_CPU==S5L8701 #define AES_BASE 0x39800000 diff --git a/firmware/target/arm/s5l8702/ipod6g/vpu-6g.c b/firmware/target/arm/s5l8702/ipod6g/vpu-6g.c new file mode 100644 index 0000000000..b5295b7059 --- /dev/null +++ b/firmware/target/arm/s5l8702/ipod6g/vpu-6g.c @@ -0,0 +1,71 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * S5L8702 VPU-B IRQ 35 completion handler. + * + * IRQ 35 is VIC1 bit 3 and is asserted when VPU-B completes a decode. + * The handler captures the completion status, acknowledges the edge, and + * releases a semaphore for the decoder driver. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "config.h" +#include "system.h" +#include "kernel.h" +#include "s5l87xx.h" + +#define VPU_IRQ_BIT (1u << 3) + +static struct semaphore vpu_complete; +static volatile uint32_t vpu_last_status1; + +void vpu_irq_init(void) +{ + semaphore_init(&vpu_complete, 1, 0); + vpu_last_status1 = 0; + VIC1INTENCLEAR = VPU_IRQ_BIT; + VIC1EDGE0 |= VPU_IRQ_BIT; + VIC1EDGE1 = VPU_IRQ_BIT; +} + +void vpu_irq_arm(void) +{ + semaphore_wait(&vpu_complete, 0); + VIC1EDGE1 = VPU_IRQ_BIT; + VIC1INTENABLE = VPU_IRQ_BIT; +} + +int vpu_irq_wait(int timeout_ticks) +{ + int ret = semaphore_wait(&vpu_complete, timeout_ticks); + VIC1INTENCLEAR = VPU_IRQ_BIT; + return ret; +} + +uint32_t vpu_irq_status1(void) +{ + return vpu_last_status1; +} + +void ICODE_ATTR INT_IRQ35(void) +{ + vpu_last_status1 = VPU_STATUS1; + VIC1EDGE1 = VPU_IRQ_BIT; + VIC1INTENCLEAR = VPU_IRQ_BIT; + semaphore_release(&vpu_complete); +} diff --git a/firmware/target/arm/s5l8702/ipod6g/vpu-6g.h b/firmware/target/arm/s5l8702/ipod6g/vpu-6g.h new file mode 100644 index 0000000000..a3a64e8aff --- /dev/null +++ b/firmware/target/arm/s5l8702/ipod6g/vpu-6g.h @@ -0,0 +1,48 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * S5L8702 VPU-B IRQ completion for H.264 hardware decoder. + * + * VPU-B asserts IRQ 35 (VIC1 bit 3, edge-triggered) on decode completion. + * These functions provide semaphore-based synchronous wait, replacing + * the sleep(200ms) polling approach. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef VPU_6G_H +#define VPU_6G_H + +#include + +/* Initialize VPU-B IRQ 35: setup semaphore, configure edge trigger. + * Call once before first decode. */ +void vpu_irq_init(void); + +/* Arm for next decode: drain stale semaphore signal, clear pending + * edge, enable IRQ 35. Call BEFORE triggering VPU decode. */ +void vpu_irq_arm(void); + +/* Wait for VPU decode completion. Blocks until ISR fires or timeout. + * Disables IRQ 35 after return. Returns OBJ_WAIT_TIMEDOUT on timeout, + * OBJ_WAIT_SUCCEEDED on success. */ +int vpu_irq_wait(int timeout_ticks); + +/* Get STATUS1 value captured by ISR during last completion. */ +uint32_t vpu_irq_status1(void); + +#endif /* VPU_6G_H */ diff --git a/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c b/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c new file mode 100644 index 0000000000..b4c5c6ea06 --- /dev/null +++ b/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c @@ -0,0 +1,1081 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025-2026 David Cormier + * + * S5L8702 VPU-B H.264 Baseline hardware decoder implementation. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "config.h" + +#ifdef HAVE_HW_H264 + +#include "system.h" +#include "kernel.h" +#include "s5l87xx.h" +#include "vpu-h264-6g.h" +#include "vpu-6g.h" + +#include + +#define VPU_CONFIG_CONST 0x82625A00 +#define VPU_SLICE_CONST 0x00110C85 +#define VPU_TRIGGER_BITS 0x88003001 +#define VPU_MODE_H264 (1u << 0) +#define VPU_CLOCK_GATE_BIT (1u << 17) +#define VPP_CLOCK_GATE_MASK (7u << 14) + +#define ALIGN32(x) CACHEALIGN_UP(x) +#define ALIGN4K(x) ALIGN_UP((x), 0x1000) +#define PHYS(x) ((uint32_t)(uintptr_t)S5L8702_PHYSICAL_ADDR(x)) +#define UNCACHED(x) S5L8702_UNCACHED_ADDR(x) + +#define BS_DMA_SIZE 262144 +#define SLICE_DESC_SIZE 320 +#define SLICE_DESC_WORDS 8 +#define VPU_MAX_SLICES \ + (SLICE_DESC_SIZE / (SLICE_DESC_WORDS * sizeof(uint32_t))) + +/* ---- Bitstream reader (exp-Golomb) ---- */ + +struct bs { + const uint8_t *buf; + unsigned long bit_offset; + unsigned long bit_length; + int error; +}; + +static uint32_t bs_un(struct bs *b, int n) +{ + uint32_t val = 0; + int i; + + if (n < 0 || n > 32) + { + b->error = 1; + return 0; + } + for (i = 0; i < n; i++) + { + if (b->bit_offset >= b->bit_length) + { + b->error = 1; + return 0; + } + unsigned long byte_pos = b->bit_offset >> 3; + unsigned int bit_pos = 7 - (b->bit_offset & 7); + val = (val << 1) | ((b->buf[byte_pos] >> bit_pos) & 1); + b->bit_offset++; + } + return val; +} + +static uint32_t bs_u1(struct bs *b) { return bs_un(b, 1); } +static uint32_t bs_u8(struct bs *b) { return bs_un(b, 8); } + +static uint32_t bs_ue(struct bs *b) +{ + int lz = 0; + while (bs_u1(b) == 0 && lz < 31) + lz++; + if (b->error || lz >= 31) + { + b->error = 1; + return 0; + } + if (lz == 0) + return 0; + if (b->bit_offset >= b->bit_length) + { + b->error = 1; + return 0; + } + return (UINT32_C(1) << lz) - 1 + bs_un(b, lz); +} + +static int bs_se(struct bs *b) +{ + uint32_t v = bs_ue(b); + return (v & 1) ? (int)((v + 1) >> 1) : -(int)(v >> 1); +} + +/* ---- EBSP to RBSP conversion ---- */ + +static int ebsp_to_rbsp(uint8_t *dst, const uint8_t *src, int src_len) +{ + int di = 0, si; + for (si = 0; si < src_len; si++) + { + if (si + 2 < src_len && src[si] == 0 && src[si+1] == 0 && + src[si+2] == 3) + { + dst[di++] = 0; + dst[di++] = 0; + si += 2; + } + else + { + dst[di++] = src[si]; + } + } + return di; +} + +/* Map an RBSP byte offset back to the corresponding EBSP byte offset. + * The VPU expects EBSP data (handles EPBs internally), so after parsing + * the slice header in RBSP space we need to find where the slice body + * starts in the original EBSP stream. */ +static int map_rbsp_to_ebsp(const uint8_t *ebsp, int ebsp_len, int rbsp_pos) +{ + int ri = 0, si = 0; + while (si < ebsp_len && ri < rbsp_pos) + { + if (si + 2 < ebsp_len && + ebsp[si] == 0 && ebsp[si+1] == 0 && ebsp[si+2] == 3) + { + if (ri + 1 >= rbsp_pos) + return si + (rbsp_pos - ri); + ri += 2; + si += 3; + } + else + { + ri++; + si++; + } + } + return si; +} + +/* ---- H.264 header structures ---- */ + +struct sps { + int profile_idc, level_idc; + int pic_width_in_mbs_minus1, pic_height_in_map_units_minus1; + int log2_max_frame_num_minus4; + int pic_order_cnt_type, log2_max_pic_order_cnt_lsb_minus4; + int max_num_ref_frames; + int gaps_allowed; + int frame_mbs_only; + int crop_left, crop_right, crop_top, crop_bottom; +}; + +struct pps { + int pic_init_qp_minus26, chroma_qp_index_offset; + int deblocking_filter_control_present_flag, weighted_pred_flag; + int num_ref_idx_l0_default_active_minus1; + int entropy_coding_mode; + int bottom_field_pic_order; + int num_slice_groups_minus1; + int weighted_bipred_idc; +}; + +struct slice_hdr { + int first_mb_in_slice, slice_type, slice_qp_delta; + int disable_deblocking_filter_idc; + int alpha_c0_offset_div2, beta_offset_div2; + int frame_num; + int num_ref_idx_l0_active_minus1; + int ref_list_modification; + int adaptive_ref_marking; + unsigned long bits_consumed; +}; + +/* ---- H.264 header parsers ---- */ + +static void parse_sps(struct sps *sps, struct bs *b) +{ + b->bit_offset = 8; + sps->profile_idc = bs_u8(b); + bs_un(b, 8); + sps->level_idc = bs_u8(b); + bs_ue(b); + + if (sps->profile_idc != 66) + { + b->error = 1; + return; + } + + sps->log2_max_frame_num_minus4 = bs_ue(b); + sps->pic_order_cnt_type = bs_ue(b); + if (sps->pic_order_cnt_type != 0) + { + b->error = 1; + return; + } + sps->log2_max_pic_order_cnt_lsb_minus4 = bs_ue(b); + sps->max_num_ref_frames = bs_ue(b); + sps->gaps_allowed = bs_u1(b); + sps->pic_width_in_mbs_minus1 = bs_ue(b); + sps->pic_height_in_map_units_minus1 = bs_ue(b); + sps->frame_mbs_only = bs_u1(b); + if (!sps->frame_mbs_only) + bs_u1(b); + bs_u1(b); /* direct_8x8_inference_flag */ + sps->crop_left = 0; + sps->crop_right = 0; + sps->crop_top = 0; + sps->crop_bottom = 0; + if (bs_u1(b)) /* frame_cropping_flag */ + { + uint32_t crop_left = bs_ue(b); + uint32_t crop_right = bs_ue(b); + uint32_t crop_top = bs_ue(b); + uint32_t crop_bottom = bs_ue(b); + + /* Baseline profile is always progressive 4:2:0 here, so both crop + * units are two pixels. */ + if (crop_left > 0x7fff || crop_right > 0x7fff || + crop_top > 0x7fff || crop_bottom > 0x7fff) + { + b->error = 1; + return; + } + sps->crop_left = (int)crop_left * 2; + sps->crop_right = (int)crop_right * 2; + sps->crop_top = (int)crop_top * 2; + sps->crop_bottom = (int)crop_bottom * 2; + } +} + +static void parse_pps(struct pps *pps, struct bs *b) +{ + b->bit_offset = 8; + bs_ue(b); bs_ue(b); + pps->entropy_coding_mode = bs_u1(b); + pps->bottom_field_pic_order = bs_u1(b); + pps->num_slice_groups_minus1 = bs_ue(b); + pps->num_ref_idx_l0_default_active_minus1 = bs_ue(b); + bs_ue(b); + pps->weighted_pred_flag = bs_u1(b); + pps->weighted_bipred_idc = bs_un(b, 2); + pps->pic_init_qp_minus26 = bs_se(b); + bs_se(b); + pps->chroma_qp_index_offset = bs_se(b); + pps->deblocking_filter_control_present_flag = bs_u1(b); +} + +static bool pps_supported(const struct pps *pps) +{ + return !pps->entropy_coding_mode && + pps->num_slice_groups_minus1 == 0 && + !pps->weighted_pred_flag && pps->weighted_bipred_idc == 0 && + pps->num_ref_idx_l0_default_active_minus1 == 0 && + pps->pic_init_qp_minus26 >= -26 && + pps->pic_init_qp_minus26 <= 25 && + pps->chroma_qp_index_offset >= -12 && + pps->chroma_qp_index_offset <= 12; +} + +static void parse_slice_header(const struct sps *sps, const struct pps *pps, + struct bs *b, int nal_type, int nal_ref_idc, + struct slice_hdr *sh) +{ + b->bit_offset = 8; + sh->first_mb_in_slice = bs_ue(b); + sh->slice_type = bs_ue(b); + if (sh->slice_type >= 5) + sh->slice_type -= 5; + bs_ue(b); /* pic_parameter_set_id */ + sh->frame_num = bs_un(b, sps->log2_max_frame_num_minus4 + 4); + + if (nal_type == 5) + bs_ue(b); /* idr_pic_id */ + if (sps->pic_order_cnt_type == 0) + { + bs_un(b, sps->log2_max_pic_order_cnt_lsb_minus4 + 4); + /* QuickTime 7.6.6's iPod preset sets + * bottom_field_pic_order_in_frame_present_flag even though the + * sequence is progressive, and writes delta_pic_order_cnt_bottom. */ + if (pps->bottom_field_pic_order) + bs_se(b); + } + + sh->num_ref_idx_l0_active_minus1 = + pps->num_ref_idx_l0_default_active_minus1; + sh->ref_list_modification = 0; + sh->adaptive_ref_marking = 0; + if (sh->slice_type == 0 || sh->slice_type == 1) + { + if (bs_u1(b)) + { + sh->num_ref_idx_l0_active_minus1 = bs_ue(b); + if (sh->slice_type == 1) + bs_ue(b); + } + } + + /* ref_pic_list_modification */ + if (sh->slice_type == 0 || sh->slice_type == 1) + { + if (bs_u1(b)) + { + uint32_t idc; + sh->ref_list_modification = 1; + do { + idc = bs_ue(b); + if (idc == 0 || idc == 1) + bs_ue(b); + else if (idc == 2) + bs_ue(b); + } while (idc != 3 && b->bit_offset < b->bit_length); + } + if (sh->slice_type == 1) + { + if (bs_u1(b)) + { + uint32_t idc; + sh->ref_list_modification = 1; + do { + idc = bs_ue(b); + if (idc == 0 || idc == 1) + bs_ue(b); + else if (idc == 2) + bs_ue(b); + } while (idc != 3 && b->bit_offset < b->bit_length); + } + } + } + + /* dec_ref_pic_marking */ + if (nal_ref_idc != 0) + { + if (nal_type == 5) + { + bs_u1(b); bs_u1(b); + } + else + { + if (bs_u1(b)) + { + uint32_t mmco; + sh->adaptive_ref_marking = 1; + do { + mmco = bs_ue(b); + if (mmco == 1 || mmco == 3) + bs_ue(b); + if (mmco == 2) + bs_ue(b); + if (mmco == 3 || mmco == 6) + bs_ue(b); + if (mmco == 4) + bs_ue(b); + } while (mmco != 0); + } + } + } + + sh->slice_qp_delta = bs_se(b); + sh->disable_deblocking_filter_idc = 0; + sh->alpha_c0_offset_div2 = 0; + sh->beta_offset_div2 = 0; + + if (pps->deblocking_filter_control_present_flag) + { + sh->disable_deblocking_filter_idc = bs_ue(b); + if (sh->disable_deblocking_filter_idc != 1) + { + sh->alpha_c0_offset_div2 = bs_se(b); + sh->beta_offset_div2 = bs_se(b); + } + } + + sh->bits_consumed = b->bit_offset; +} + +/* ---- Slice descriptor builder ---- */ + +static void build_slice_descriptor(uint32_t *desc, + int first_mb, + int mb_count, + int slice_type, + int slice_qp_y, + int chroma_qp_off, + int weighted_pred, + int num_ref_l0, + int deblk_idc, + int alpha_off, + int beta_off, + int bit_offset, + uint32_t bs_phys_addr, + int bs_length) +{ + /* Word 0 carries the first macroblock and the number covered by this + * descriptor. Apple's iPod preset emits two half-picture slices. */ + desc[0] = ((first_mb & 0x7FF) << 21) + | ((mb_count & 0x7FF) << 10) + | ((slice_type & 0xF) << 6) + | (slice_qp_y & 0x3F); + + desc[1] = ((chroma_qp_off & 0xFF) << 15) + | ((weighted_pred & 1) << 14) + | ((num_ref_l0 & 0xF) << 10) + | ((deblk_idc & 0x3) << 8) + | ((alpha_off & 0xF) << 4) + | (beta_off & 0xF); + + desc[2] = VPU_SLICE_CONST; + desc[3] = 0; + desc[4] = bit_offset & 0xFF; + desc[5] = bs_phys_addr; + desc[6] = bs_length; + desc[7] = 0; +} + +/* ---- VPU-B power management ---- */ + +static void vpub_power_on(void) +{ + uint16_t cg; + uint32_t pw; + + /* Gate VPP clocks + VPU-B clock as initial state */ + VPU_MODE &= ~VPU_MODE_H264; + PWRCON(0) |= VPU_CLOCK_GATE_BIT | VPP_CLOCK_GATE_MASK; + /* No delay: Apple FW, all Rockbox drivers use zero delay after PWRCON */ + + /* Enable CG16_SVID (PLL2 source, already locked as system clock) */ + cg = CG16_SVID; + cg &= ~(CG16_DISABLE_BIT | (CG16_SEL_MSK << CG16_SEL_POS)); + cg |= CG16_SEL_PLL2 << CG16_SEL_POS; + CG16_SVID = cg; + + /* Enable VPP clocks */ + pw = PWRCON(0); + PWRCON(0) = pw & ~VPP_CLOCK_GATE_MASK; + + /* H.264 mode */ + VPU_MODE |= VPU_MODE_H264; + + /* Zero stale registers */ + PWRCON(0) &= ~VPU_CLOCK_GATE_BIT; + { + volatile uint32_t *base = (volatile uint32_t *)VPU_BASE; + int i; + for (i = 0; i < 0x130/4; i++) + base[i] = 0; + } + PWRCON(0) |= VPU_CLOCK_GATE_BIT; + + /* Initialize IRQ 35 for completion signaling */ + vpu_irq_init(); +} + +static void vpub_power_off(void) +{ + VPU_MODE &= ~VPU_MODE_H264; + PWRCON(0) |= VPU_CLOCK_GATE_BIT | VPP_CLOCK_GATE_MASK; +} + +/* ---- VPU-B decode trigger ---- */ + +static int vpub_decode(uint32_t ctrl_phys, uint32_t desc_phys, + uint32_t y_phys, uint32_t cb_phys, uint32_t cr_phys, + const uint32_t *dpb_y, const uint32_t *dpb_cb, + const uint32_t *dpb_cr, int dpb_count, + int width_mbs, int height_mbs, + int pic_w, int num_slices) +{ + uint32_t val; + int ret, old_irq; + + /* Double clock enable (Apple: 0x1c0b94 + 0x1c0ba4) */ + old_irq = disable_irq_save(); + PWRCON(0) &= ~VPU_CLOCK_GATE_BIT; + restore_irq(old_irq); + old_irq = disable_irq_save(); + PWRCON(0) &= ~VPU_CLOCK_GATE_BIT; + restore_irq(old_irq); + + /* Re-set VPU_MODE every frame (Apple: 0x1c0bac) */ + VPU_MODE |= VPU_MODE_H264; + + /* vtable[0x48] = h264_vpu_stop_and_reset (0x001c0b4c) */ + { + volatile uint32_t *base = (volatile uint32_t *)VPU_BASE; + uint32_t ec_val; + int i; + ec_val = base[0xEC/4]; + base[0xEC/4] = ec_val & ~1u; + ec_val = base[0xEC/4]; + (void)ec_val; + for (i = 0; i < 0x130/4; i++) + base[i] = 0; + } + + /* Program registers (Apple's exact order from FUN_001c06ac) */ + VPU_CTRL_BUF = ctrl_phys; + val = VPU_DIMS; + val = (val & ~0x3F00) | ((height_mbs & 0x3F) << 8); + VPU_DIMS = val; + val = VPU_DIMS; + val = (val & ~0x003F) | (width_mbs & 0x3F); + VPU_DIMS = val; + val = VPU_CTRL; + val = (val & ~0x0FFE) | ((width_mbs * height_mbs * 2) & 0xFFE); + VPU_CTRL = val; + VPU_SLICE_DESC = desc_phys; + val = VPU_STRIDES; + val = (val & ~0x01FF0000) | (((pic_w / 2) & 0x1FF) << 16); + VPU_STRIDES = val; + val = VPU_STRIDES; + val = (val & ~0x000003FF) | (pic_w & 0x3FF); + VPU_STRIDES = val; + VPU_OUT_Y = y_phys; + VPU_OUT_CB = cb_phys; + VPU_OUT_CR = cr_phys; + VPU_CONFIG = VPU_CONFIG_CONST; + + /* Fill the DPB reference frame table with reference frames + * (newest at slot 0). Remaining slots get slot 0's data. */ + { + int dpb; + for (dpb = 0; dpb < dpb_count && dpb < 17; dpb++) + { + VPU_DPB_Y(dpb) = dpb_y[dpb]; + VPU_DPB_CB(dpb) = dpb_cb[dpb]; + VPU_DPB_CR(dpb) = dpb_cr[dpb]; + } + for (; dpb < 17; dpb++) + { + VPU_DPB_Y(dpb) = dpb_count > 0 ? dpb_y[0] : 0; + VPU_DPB_CB(dpb) = dpb_count > 0 ? dpb_cb[0] : 0; + VPU_DPB_CR(dpb) = dpb_count > 0 ? dpb_cr[0] : 0; + } + } + + /* Slice count + trigger */ + val = VPU_CTRL; + val = (val & ~0x07FF0000) | ((num_slices & 0x7FF) << 16); + VPU_CTRL = val; + + /* Arm IRQ, trigger, wait */ + vpu_irq_arm(); + VPU_CTRL = VPU_CTRL | VPU_TRIGGER_BITS; + + ret = vpu_irq_wait(HZ / 2); + if (ret == OBJ_WAIT_TIMEDOUT) + { + old_irq = disable_irq_save(); + PWRCON(0) |= VPU_CLOCK_GATE_BIT; + restore_irq(old_irq); + return -1; + } + + val = vpu_irq_status1(); + ret = 0; + if ((val << 3) >> 21) + ret = -2; + + if (ret == 0) + VPU_CONFIG = VPU_CONFIG_CONST; + + /* Disable clock */ + old_irq = disable_irq_save(); + PWRCON(0) |= VPU_CLOCK_GATE_BIT; + restore_irq(old_irq); + + return ret; +} + +/* ---- Internal context structure ---- */ + +#define MAX_DPB_FRAMES 5 /* up to 4 reference frames + 1 output */ + +struct vpu_h264 { + int max_w, max_h; + int pic_w, pic_h, pic_wmb, pic_hmb; + int display_w, display_h; + struct sps sps; + struct pps pps; + int have_sps, have_pps; + int cur_out; /* next output buffer index */ + int last_out; /* most recently decoded picture */ + int dpb_count; /* valid references in dpb_order */ + int dpb_order[MAX_DPB_FRAMES]; /* buffer indices, oldest first */ + uint8_t *ctrl_buf; + uint8_t *slice_desc; + uint8_t *bs_dma; + uint8_t *nalu_buf; + uint8_t *frame_y[MAX_DPB_FRAMES]; + uint8_t *frame_cb[MAX_DPB_FRAMES]; + uint8_t *frame_cr[MAX_DPB_FRAMES]; + int frame_y_size; + int frame_cb_size; + int frame_cr_size; +}; + +static int apply_sps_dimensions(struct vpu_h264 *v) +{ + int horizontal_crop; + int vertical_crop; + + if (v->sps.profile_idc != 66 || v->sps.level_idc > 30 || + v->sps.pic_order_cnt_type != 0 || + v->sps.log2_max_frame_num_minus4 < 0 || + v->sps.log2_max_frame_num_minus4 > 12 || + v->sps.log2_max_pic_order_cnt_lsb_minus4 < 0 || + v->sps.log2_max_pic_order_cnt_lsb_minus4 > 12 || + v->sps.max_num_ref_frames < 0 || + v->sps.max_num_ref_frames > 2 || v->sps.gaps_allowed || + !v->sps.frame_mbs_only || + v->sps.pic_width_in_mbs_minus1 < 0 || + v->sps.pic_width_in_mbs_minus1 >= v->max_w / 16 || + v->sps.pic_height_in_map_units_minus1 < 0 || + v->sps.pic_height_in_map_units_minus1 >= v->max_h / 16) + return -1; + + v->pic_wmb = v->sps.pic_width_in_mbs_minus1 + 1; + v->pic_hmb = v->sps.pic_height_in_map_units_minus1 + 1; + v->pic_w = v->pic_wmb * 16; + v->pic_h = v->pic_hmb * 16; + horizontal_crop = v->sps.crop_left + v->sps.crop_right; + vertical_crop = v->sps.crop_top + v->sps.crop_bottom; + v->display_w = v->pic_w - horizontal_crop; + v->display_h = v->pic_h - vertical_crop; + + if (horizontal_crop < 0 || horizontal_crop >= v->pic_w || + vertical_crop < 0 || vertical_crop >= v->pic_h || + v->display_w <= 0 || v->display_h <= 0 || + (v->display_w & 1) || (v->display_h & 1)) + return -1; + + v->frame_y_size = v->pic_w * v->pic_h; + v->frame_cb_size = (v->pic_w / 2) * (v->pic_h / 2); + v->frame_cr_size = v->frame_cb_size; + return 0; +} + +/* ---- Public API ---- */ + +size_t vpu_h264_buf_size(int max_w, int max_h) +{ + size_t sz = 0; + + if (max_w <= 0 || max_h <= 0 || max_w > 1280 || max_h > 720) + return 0; + sz += sizeof(struct vpu_h264) + 32; + sz += (size_t)max_w * max_h * 3 / 2 + 4096; /* ctrl_buf */ + sz += SLICE_DESC_SIZE + 32; + sz += BS_DMA_SIZE + 32; + sz += BS_DMA_SIZE + 32; /* nalu_buf */ + sz += ((size_t)max_w * max_h + 4096) * MAX_DPB_FRAMES; /* frame_y */ + sz += ((size_t)max_w * max_h / 4 + 32) * MAX_DPB_FRAMES; + sz += ((size_t)max_w * max_h / 4 + 32) * MAX_DPB_FRAMES; + return sz; +} + +struct vpu_h264 *vpu_h264_open(void *buf, size_t buf_size, + int max_w, int max_h) +{ + uint8_t *p = (uint8_t *)buf; + struct vpu_h264 *v; + int fy_sz, fc_sz, ctrl_sz; + + if (max_w <= 0 || max_h <= 0 || max_w > 1280 || max_h > 720) + return NULL; + if (!buf || buf_size < vpu_h264_buf_size(max_w, max_h)) + return NULL; + + v = (struct vpu_h264 *)ALIGN32(p); + p = (uint8_t *)v + sizeof(*v); + memset(v, 0, sizeof(*v)); + + v->max_w = max_w; + v->max_h = max_h; + + ctrl_sz = max_w * max_h * 3 / 2; + fy_sz = max_w * max_h; + fc_sz = max_w * max_h / 4; + + v->ctrl_buf = (uint8_t *)ALIGN4K(p); p = v->ctrl_buf + ctrl_sz; + v->slice_desc = (uint8_t *)ALIGN32(p); + p = v->slice_desc + SLICE_DESC_SIZE; + v->bs_dma = (uint8_t *)ALIGN32(p); p = v->bs_dma + BS_DMA_SIZE; + v->nalu_buf = (uint8_t *)ALIGN32(p); p = v->nalu_buf + BS_DMA_SIZE; + + { + int i; + for (i = 0; i < MAX_DPB_FRAMES; i++) + { + v->frame_y[i] = (uint8_t *)ALIGN4K(p); p = v->frame_y[i] + fy_sz; + v->frame_cb[i] = (uint8_t *)ALIGN32(p); p = v->frame_cb[i] + fc_sz; + v->frame_cr[i] = (uint8_t *)ALIGN32(p); p = v->frame_cr[i] + fc_sz; + } + } + + v->frame_y_size = fy_sz; + v->frame_cb_size = fc_sz; + v->frame_cr_size = fc_sz; + v->cur_out = 0; + v->dpb_count = 0; + + /* Zero control buffers */ + memset(v->ctrl_buf, 0, ctrl_sz); + memset(v->bs_dma, 0, BS_DMA_SIZE); + + /* Pre-fill frame buffers with neutral YCbCr (black) */ + { + int i; + for (i = 0; i < MAX_DPB_FRAMES; i++) + { + memset(v->frame_y[i], 0x10, fy_sz); + memset(v->frame_cb[i], 0x80, fc_sz); + memset(v->frame_cr[i], 0x80, fc_sz); + } + } + + /* Make all cached initialization visible before the VPU uses DMA. */ + commit_dcache(); + vpub_power_on(); + return v; +} + +int vpu_h264_configure(struct vpu_h264 *v, + const uint8_t *avcc, int avcc_len) +{ + int i, cnt, offset; + int nalu_len_size; + struct bs b; + + if (!v || !avcc || avcc_len < 7) + return -1; + + /* avcC structure: version(1) profile(1) compat(1) level(1) + * nalu_len_size(1) num_sps(1) [sps_len(2) sps_data]... + * num_pps(1) [pps_len(2) pps_data]... */ + nalu_len_size = (avcc[4] & 0x03) + 1; + (void)nalu_len_size; + + /* Parse SPS */ + cnt = avcc[5] & 0x1F; + offset = 6; + for (i = 0; i < cnt && offset + 2 <= avcc_len; i++) + { + int sps_len = (avcc[offset] << 8) | avcc[offset + 1]; + offset += 2; + if (sps_len < 1 || offset + sps_len > avcc_len || + (avcc[offset] & 0x9f) != 7) + return -1; + int rbsp_len = ebsp_to_rbsp(v->nalu_buf, avcc + offset, sps_len); + b.buf = v->nalu_buf; + b.bit_offset = 0; + b.bit_length = rbsp_len * 8; + b.error = 0; + parse_sps(&v->sps, &b); + + if (b.error || apply_sps_dimensions(v) < 0) + return -1; + v->have_sps = 1; + offset += sps_len; + } + + /* Parse PPS */ + if (offset >= avcc_len) + return -1; + cnt = avcc[offset++]; + for (i = 0; i < cnt && offset + 2 <= avcc_len; i++) + { + int pps_len = (avcc[offset] << 8) | avcc[offset + 1]; + offset += 2; + if (pps_len < 1 || offset + pps_len > avcc_len || + (avcc[offset] & 0x9f) != 8) + return -1; + int rbsp_len = ebsp_to_rbsp(v->nalu_buf, avcc + offset, pps_len); + b.buf = v->nalu_buf; + b.bit_offset = 0; + b.bit_length = rbsp_len * 8; + b.error = 0; + parse_pps(&v->pps, &b); + if (b.error || !pps_supported(&v->pps)) + return -1; + v->have_pps = 1; + offset += pps_len; + } + + return (v->have_sps && v->have_pps) ? 0 : -1; +} + +struct picture_slice { + const uint8_t *nalu; + int nalu_len; + int nal_type; + int nal_ref_idc; + struct slice_hdr header; +}; + +static int parse_picture_slice(struct vpu_h264 *v, const uint8_t *nalu, + int nalu_len, struct picture_slice *slice) +{ + struct bs b; + int rbsp_len; + + if (!v->have_sps || !v->have_pps || nalu_len < 1 || + (nalu[0] & 0x80) != 0) + return -1; + rbsp_len = ebsp_to_rbsp(v->nalu_buf, nalu, MIN(nalu_len, 256)); + slice->nalu = nalu; + slice->nalu_len = nalu_len; + slice->nal_type = v->nalu_buf[0] & 0x1f; + slice->nal_ref_idc = (v->nalu_buf[0] >> 5) & 3; + b.buf = v->nalu_buf; + b.bit_offset = 0; + b.bit_length = rbsp_len * 8; + b.error = 0; + parse_slice_header(&v->sps, &v->pps, &b, slice->nal_type, + slice->nal_ref_idc, &slice->header); + if (b.error || + (slice->header.slice_type != 0 && slice->header.slice_type != 2) || + slice->header.num_ref_idx_l0_active_minus1 != 0 || + slice->header.ref_list_modification || + slice->header.adaptive_ref_marking || + slice->header.disable_deblocking_filter_idc < 0 || + slice->header.disable_deblocking_filter_idc > 2 || + slice->header.alpha_c0_offset_div2 < -6 || + slice->header.alpha_c0_offset_div2 > 6 || + slice->header.beta_offset_div2 < -6 || + slice->header.beta_offset_div2 > 6) + return -1; + return 0; +} + +static int decode_picture(struct vpu_h264 *v, struct picture_slice *slices, + int slice_count) +{ + uint32_t dpb_y[MAX_DPB_FRAMES], dpb_cb[MAX_DPB_FRAMES]; + uint32_t dpb_cr[MAX_DPB_FRAMES]; + int total_mbs = v->pic_wmb * v->pic_hmb; + int out_buf = v->cur_out; + int dpb_fill = 0; + int dma_used = 0; + int is_idr; + int ret; + int i; + + if (slice_count < 1 || slice_count > (int)VPU_MAX_SLICES || + slices[0].header.first_mb_in_slice != 0) + return -1; + is_idr = slices[0].nal_type == 5; + for (i = 0; i < slice_count; i++) + { + if ((slices[i].nal_type != 1 && slices[i].nal_type != 5) || + (slices[i].nal_type == 5) != is_idr || + slices[i].nal_ref_idc != slices[0].nal_ref_idc || + slices[i].header.frame_num != slices[0].header.frame_num || + slices[i].header.slice_type != slices[0].header.slice_type || + slices[i].header.first_mb_in_slice < 0 || + slices[i].header.first_mb_in_slice >= total_mbs || + (i > 0 && slices[i].header.first_mb_in_slice <= + slices[i - 1].header.first_mb_in_slice)) + return -1; + } + + if (is_idr) + v->dpb_count = 0; + for (i = v->dpb_count - 1; i >= 0 && dpb_fill < MAX_DPB_FRAMES; i--) + { + int bi = v->dpb_order[i]; + dpb_y[dpb_fill] = PHYS(v->frame_y[bi]); + dpb_cb[dpb_fill] = PHYS(v->frame_cb[bi]); + dpb_cr[dpb_fill] = PHYS(v->frame_cr[bi]); + dpb_fill++; + } + + memset(UNCACHED(v->slice_desc), 0, SLICE_DESC_SIZE); + for (i = 0; i < slice_count; i++) + { + struct slice_hdr *sh = &slices[i].header; + int next_mb = i + 1 < slice_count ? + slices[i + 1].header.first_mb_in_slice : total_mbs; + int mb_count = next_mb - sh->first_mb_in_slice; + int slice_qp = 26 + v->pps.pic_init_qp_minus26 + + sh->slice_qp_delta; + int hdr_bytes = sh->bits_consumed / 8; + int bit_off = sh->bits_consumed & 7; + int ebsp_hdr_offset = map_rbsp_to_ebsp( + slices[i].nalu, slices[i].nalu_len, hdr_bytes); + int dma_len = slices[i].nalu_len - ebsp_hdr_offset; + int dma_offset = (dma_used + 31) & ~31; + + if (mb_count <= 0 || slice_qp < 0 || slice_qp > 51 || + dma_len <= 0 || dma_offset > BS_DMA_SIZE - dma_len) + return -1; + memcpy(UNCACHED(v->bs_dma + dma_offset), + slices[i].nalu + ebsp_hdr_offset, dma_len); + build_slice_descriptor( + (uint32_t *)UNCACHED(v->slice_desc) + + i * SLICE_DESC_WORDS, + sh->first_mb_in_slice, mb_count, sh->slice_type, slice_qp, + v->pps.chroma_qp_index_offset, v->pps.weighted_pred_flag, + sh->num_ref_idx_l0_active_minus1, + sh->disable_deblocking_filter_idc, + sh->alpha_c0_offset_div2, sh->beta_offset_div2, + bit_off, PHYS(v->bs_dma + dma_offset), dma_len); + dma_used = dma_offset + dma_len; + } + + ret = vpub_decode( + PHYS(v->ctrl_buf), PHYS(v->slice_desc), + PHYS(v->frame_y[out_buf]), PHYS(v->frame_cb[out_buf]), + PHYS(v->frame_cr[out_buf]), dpb_y, dpb_cb, dpb_cr, dpb_fill, + v->pic_wmb, v->pic_hmb, v->pic_w, slice_count); + commit_discard_dcache(); + if (ret != 0) + return -1; + + v->last_out = out_buf; + if (slices[0].nal_ref_idc != 0 && v->sps.max_num_ref_frames > 0) + { + int max_ref = MIN(v->sps.max_num_ref_frames, + MAX_DPB_FRAMES - 1); + while (v->dpb_count >= max_ref) + { + for (i = 0; i < v->dpb_count - 1; i++) + v->dpb_order[i] = v->dpb_order[i + 1]; + v->dpb_count--; + } + v->dpb_order[v->dpb_count++] = out_buf; + } + v->cur_out = (out_buf + 1) % MAX_DPB_FRAMES; + return 1; +} + +int vpu_h264_decode_nalu(struct vpu_h264 *v, + const uint8_t *nalu, int nalu_len) +{ + struct picture_slice slice; + struct bs b; + int rbsp_len; + int nal_type; + + if (!v || !nalu || nalu_len < 1 || (nalu[0] & 0x80) != 0) + return -1; + rbsp_len = ebsp_to_rbsp(v->nalu_buf, nalu, MIN(nalu_len, 256)); + nal_type = v->nalu_buf[0] & 0x1f; + b.buf = v->nalu_buf; + b.bit_offset = 0; + b.bit_length = rbsp_len * 8; + b.error = 0; + + if (nal_type == 7) + { + parse_sps(&v->sps, &b); + if (b.error || apply_sps_dimensions(v) < 0) + return -1; + v->have_sps = 1; + return 0; + } + if (nal_type == 8) + { + parse_pps(&v->pps, &b); + if (b.error || !pps_supported(&v->pps)) + return -1; + v->have_pps = 1; + return 0; + } + if (nal_type == 1 || nal_type == 5) + { + if (parse_picture_slice(v, nalu, nalu_len, &slice) < 0) + return -1; + return decode_picture(v, &slice, 1); + } + return 0; +} + +int vpu_h264_decode_sample(struct vpu_h264 *v, const uint8_t *sample, + int sample_len, int nalu_len_size) +{ + struct picture_slice slices[VPU_MAX_SLICES]; + int slice_count = 0; + int position = 0; + + if (!v || !sample || sample_len <= 0 || + nalu_len_size < 1 || nalu_len_size > 4) + return -1; + while (position + nalu_len_size <= sample_len) + { + uint32_t nalu_len = 0; + int nal_type; + int i; + + for (i = 0; i < nalu_len_size; i++) + nalu_len = (nalu_len << 8) | sample[position + i]; + position += nalu_len_size; + if (nalu_len == 0 || nalu_len > (uint32_t)(sample_len - position)) + return -1; + if ((sample[position] & 0x80) != 0) + return -1; + nal_type = sample[position] & 0x1f; + if (nal_type == 1 || nal_type == 5) + { + if (slice_count >= (int)VPU_MAX_SLICES || + parse_picture_slice(v, sample + position, nalu_len, + &slices[slice_count]) < 0) + return -1; + slice_count++; + } + else if (nal_type == 7 || nal_type == 8) + { + if (vpu_h264_decode_nalu(v, sample + position, nalu_len) < 0) + return -1; + } + position += nalu_len; + } + if (position != sample_len) + return -1; + return slice_count > 0 ? decode_picture(v, slices, slice_count) : 0; +} + +void vpu_h264_get_frame(const struct vpu_h264 *v, + const uint8_t **y, const uint8_t **cb, + const uint8_t **cr, int *w, int *h, int *stride) +{ + int luma_offset = v->sps.crop_top * v->pic_w + v->sps.crop_left; + int chroma_offset = (v->sps.crop_top / 2) * (v->pic_w / 2) + + v->sps.crop_left / 2; + + if (y) + *y = v->frame_y[v->last_out] + luma_offset; + if (cb) + *cb = v->frame_cb[v->last_out] + chroma_offset; + if (cr) + *cr = v->frame_cr[v->last_out] + chroma_offset; + if (w) + *w = v->display_w; + if (h) + *h = v->display_h; + if (stride) + *stride = v->pic_w; +} + +void vpu_h264_close(struct vpu_h264 *v) +{ + if (v) + vpub_power_off(); +} + +const struct hw_h264_api target_hw_h264_api = { + .buf_size = vpu_h264_buf_size, + .open = vpu_h264_open, + .configure = vpu_h264_configure, + .decode_sample = vpu_h264_decode_sample, + .get_frame = vpu_h264_get_frame, + .close = vpu_h264_close, +}; + +#endif /* HAVE_HW_H264 */ diff --git a/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.h b/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.h new file mode 100644 index 0000000000..b5f9b65ddf --- /dev/null +++ b/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.h @@ -0,0 +1,84 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025-2026 David Cormier + * + * S5L8702 VPU-B H.264 Baseline hardware decoder. + * + * Drives the VPU-B block on iPod Classic 6G (S5L8702). + * Full hardware decode: CAVLC, dequant, IDCT, intra/inter prediction, + * motion compensation, and in-loop deblocking. + * + * Supports: H.264 Baseline profile, level <= 3.0, up to ten ordered slices + * per frame. The single-slice path is proven bit-perfect against ffmpeg; + * multi-slice support follows the same Apple VPU descriptor interface. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef VPU_H264_6G_H +#define VPU_H264_6G_H + +#include "hw_h264.h" + +#include +#include + +/* Returns minimum buffer size needed for given max dimensions. + * Caller must provide a contiguous buffer of at least this size + * to vpu_h264_open(). Accounts for alignment padding. */ +size_t vpu_h264_buf_size(int max_w, int max_h); + +/* Initialize decoder. Allocates internal buffers from buf via bump + * allocator. Powers on VPU-B hardware and IRQ. + * Returns context pointer (inside buf) or NULL on error. */ +struct vpu_h264 *vpu_h264_open(void *buf, size_t buf_size, + int max_w, int max_h); + +/* Feed avcC decoder configuration (from MP4 container). + * Parses SPS and PPS NALUs from the avcC blob. + * Returns 0 on success, -1 on error. */ +int vpu_h264_configure(struct vpu_h264 *v, + const uint8_t *avcc, int avcc_len); + +/* Feed one raw NALU (including nal_header byte, excluding length prefix). + * Accepts EBSP (emulation prevention bytes intact). + * Handles SPS (7), PPS (8), IDR slice (5), non-IDR slice (1) internally. + * For slice NALUs: triggers synchronous HW decode (~5ms). + * + * Returns: 1 = frame decoded (call vpu_h264_get_frame) + * 0 = NALU consumed (SPS/PPS/SEI, no frame output) + * -1 = error (STATUS1 bits or timeout) */ +int vpu_h264_decode_nalu(struct vpu_h264 *v, + const uint8_t *nalu, int nalu_len); + +/* Decode one length-prefixed AVC sample as a complete picture. All slices + * are submitted together, matching the two-slice files produced by iTunes' + * "Create iPod or iPhone Version" action. */ +int vpu_h264_decode_sample(struct vpu_h264 *v, const uint8_t *sample, + int sample_len, int nalu_len_size); + +/* Get pointers to last decoded frame planes (YCbCr 4:2:0 planar). + * Pointers are cached aliases; cache is already invalidated. + * stride is the coded luma pitch; w/h are the cropped display dimensions. + * Valid until the next decode call. */ +void vpu_h264_get_frame(const struct vpu_h264 *v, + const uint8_t **y, const uint8_t **cb, + const uint8_t **cr, int *w, int *h, int *stride); + +/* Shutdown decoder. Powers off VPU-B hardware. */ +void vpu_h264_close(struct vpu_h264 *v); + +#endif /* VPU_H264_6G_H */ diff --git a/firmware/target/arm/s5l8702/system-s5l8702.c b/firmware/target/arm/s5l8702/system-s5l8702.c index a706e81491..8b6a314ce9 100644 --- a/firmware/target/arm/s5l8702/system-s5l8702.c +++ b/firmware/target/arm/s5l8702/system-s5l8702.c @@ -110,6 +110,9 @@ static const struct clocking_mode clk_modes[] = /* cdiv hdiv hprat hsdiv */ /* CClk HClk PClk SM1Clk FPS */ { 1, 2, 2, 4 }, /* 216 108 54 27 42 */ #ifdef HAVE_ADJUSTABLE_CPU_FREQ + /* Preserve the qualified intermediate step used when changing between + * the normal and boosted clocks. */ + { 2, 2, 2, 4 }, /* 108 108 54 27 */ { 4, 4, 2, 2 }, /* 54 54 27 27 21 */ #endif }; diff --git a/manual/platform/ipod6g.tex b/manual/platform/ipod6g.tex index fc77e03a25..078d7a75c1 100644 --- a/manual/platform/ipod6g.tex +++ b/manual/platform/ipod6g.tex @@ -5,6 +5,7 @@ \edef\UseOption{\UseOption,HAVE_DISK_STORAGE} \edef\UseOption{\UseOption,HAVE_CAR_ADAPTER_MODE} \edef\UseOption{\UseOption,HAVE_COMPOSITE_VIDEO_OUT} +\edef\UseOption{\UseOption,HAVE_HW_H264} \edef\UseOption{\UseOption,ipod} \newcommand{\playerman}{Ipod} diff --git a/manual/plugins/h264_player.tex b/manual/plugins/h264_player.tex new file mode 100644 index 0000000000..fb530bd309 --- /dev/null +++ b/manual/plugins/h264_player.tex @@ -0,0 +1,26 @@ +\subsection{H.264 Player} + +The H.264 Player uses a supported player's hardware video decoder to play +H.264 video from \fname{.mp4}, \fname{.m4v}, and \fname{.mov} files. It +supports H.264 Baseline Profile through level 3.0 at resolutions up to +640x480. AAC-LC audio is supported. + +Select a supported file in the Rockbox \setting{File Browser} to start +playback. Playback resumes from the last saved position unless the previous +playback reached the end or the final five percent of the file. + +\begin{btnmap} + \ActionWpsPlay + & Pause / Resume\\ + \ActionWpsVolUp{} / \ActionWpsVolDown + & Adjust volume up / down\\ + \ActionWpsSkipPrev{} / \ActionWpsSkipNext + & Seek backward / forward by 10 seconds\\ + \ActionWpsMenu + & Open the H.264 Player menu\\ + \ActionWpsStop + & Exit the player\\ +\end{btnmap} + +The player menu can resume playback or exit the player. A resume point is +saved when playback is stopped before the final five percent of the file. diff --git a/manual/plugins/main.tex b/manual/plugins/main.tex index 1da6c16b59..dd07a4eba1 100644 --- a/manual/plugins/main.tex +++ b/manual/plugins/main.tex @@ -182,6 +182,9 @@ option from the \setting{Context Menu} (see \reference{ref:Contextmenu}).} & \fname{.s3m, .stm, .stx, .ult,} & \\ & \fname{.uni, .umx, .xm} & \\ MPEG Player & \fname{.mpg, .mpeg, .mpv, .m2v} & \\ + } + \opt{HAVE_HW_H264}{ + H.264 Player & \fname{.mp4, .m4v, .mov} & \\ } MP3 Encoder & \fname{.wav} & x \\ Rockboy & \fname{.gb, .gbc} & \\ @@ -211,6 +214,8 @@ option from the \setting{Context Menu} (see \reference{ref:Contextmenu}).} \nopt{lowmem}{\input{plugins/mpegplayer.tex}} +\opt{HAVE_HW_H264}{\input{plugins/h264_player.tex}} + \input{plugins/mp3_encoder.tex} \opt{iriverh300,iriverh100,SANSA_FUZE_PAD,SANSA_E200_PAD,IPOD_4G_PAD,IPOD_3G_PAD% From b7fe01393ac94d10911b53f7fb45f6321234740d Mon Sep 17 00:00:00 2001 From: Jared Sanchez Date: Fri, 11 Sep 2026 15:52:56 -0700 Subject: [PATCH 167/408] usb-drv-arc: defer SET_ADDRESS until status completion The generic USB core calls usb_drv_set_address() after starting the zero-length status stage for SET_ADDRESS. On an iPod Video 5.5G connected to a V-MODA VAMP VERZA, applying the address at that point causes the accessory to reset the USB bus and enumeration does not continue. Keep the generic USB core unchanged. In the ARC driver, capture a standard device SET_ADDRESS request when its setup packet arrives and defer writing REG_DEVICEADDR until the successful zero-length EP0 IN status transfer completes. Clear any pending address on a new setup packet or USB bus reset. With this driver-specific change the VAMP completes enumeration, iAP authentication, 44.1 kHz digital-audio negotiation, and audio playback. Tested on real hardware: - Apple iPod Video 5.5G - V-MODA VAMP VERZA over 30-pin USB - 44.1 kHz digital audio playback Development provenance: The debugging process and patch preparation were performed with assistance from OpenAI ChatGPT (GPT-5.6 Sol). I built and tested the firmware on the hardware and verified the final behavior. Change-Id: Ic9392d211dccda568fd42b06cb2b8853d4b8e893 --- firmware/target/arm/usb-drv-arc.c | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/firmware/target/arm/usb-drv-arc.c b/firmware/target/arm/usb-drv-arc.c index 9707ef8330..dd46d67dd0 100644 --- a/firmware/target/arm/usb-drv-arc.c +++ b/firmware/target/arm/usb-drv-arc.c @@ -343,6 +343,8 @@ struct queue_head { static struct queue_head qh_array[USB_NUM_ENDPOINTS*2] USB_QHARRAY_ATTR; +static int pending_device_address = -1; + static struct semaphore transfer_completion_signal[USB_NUM_ENDPOINTS*2] SHAREDBSS_ATTR; @@ -677,7 +679,9 @@ bool usb_drv_powered(void) void usb_drv_set_address(int address) { - REG_DEVICEADDR = address << USBDEVICEADDRESS_BIT_POS; + /* SET_ADDRESS is captured when the setup packet arrives and applied + * after the EP0 IN status stage completes. */ + (void)address; } void usb_drv_reset_endpoint(int endpoint, bool send) @@ -1080,7 +1084,17 @@ static void control_received(void) } } - usb_core_setup_received((struct usb_ctrlrequest*)tmp); + struct usb_ctrlrequest *req = (struct usb_ctrlrequest*)tmp; + + /* A new setup packet supersedes any unfinished SET_ADDRESS request. */ + pending_device_address = -1; + + if ((req->bRequestType & (USB_TYPE_MASK | USB_RECIP_MASK)) == + (USB_TYPE_STANDARD | USB_RECIP_DEVICE) && + req->bRequest == USB_REQ_SET_ADDRESS) + pending_device_address = req->wValue; + + usb_core_setup_received(req); } static void transfer_completed(void) @@ -1113,6 +1127,14 @@ static void transfer_completed(void) ((td->size_ioc_sts & DTD_PACKET_SIZE) >> DTD_LENGTH_BIT_POS)); td=(struct transfer_descriptor*) td->next_td_ptr; } + if (ep == EP_CONTROL && dir == DIR_IN && + pending_device_address >= 0 && + qh->status == 0 && length == 0) { + REG_DEVICEADDR = + pending_device_address << USBDEVICEADDRESS_BIT_POS; + pending_device_address = -1; + } + if(qh->wait) { qh->wait=0; semaphore_release(&transfer_completion_signal[pipe]); @@ -1134,6 +1156,7 @@ static void bus_reset(void) logf("usb bus_reset"); REG_DEVICEADDR = 0; + pending_device_address = -1; REG_ENDPTSETUPSTAT = REG_ENDPTSETUPSTAT; REG_ENDPTCOMPLETE = REG_ENDPTCOMPLETE; From a7ab67f4591bece15577dfe71d6644d521b6ff19 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Sun, 13 Sep 2026 12:03:33 -0400 Subject: [PATCH 168/408] add backlight_on_button_hold to plugins and backlight_use_settings_helper backlight_on_button_hold / remote_backlight_on_button_hold were never added to the Plugin API nor backlight_use_settings() helper Change-Id: I75999af76f98244a870ab86564e591c8a900e66c --- apps/plugin.c | 4 ++++ apps/plugin.h | 4 ++++ apps/plugins/lib/helper.c | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/apps/plugin.c b/apps/plugin.c index 2e747fca61..e8723b3aa1 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -885,6 +885,10 @@ static const struct plugin_api rockbox_api = { #endif #ifdef HAVE_HW_H264 &target_hw_h264_api, +#endif + backlight_set_on_button_hold, +#ifdef HAS_REMOTE_BUTTON_HOLD + remote_backlight_set_on_button_hold, #endif }; diff --git a/apps/plugin.h b/apps/plugin.h index 46e32d8b9b..3590786006 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -1051,6 +1051,10 @@ struct plugin_api { #endif #ifdef HAVE_HW_H264 const struct hw_h264_api *hw_h264; +#endif + void (*backlight_set_on_button_hold)(int index); +#ifdef HAS_REMOTE_BUTTON_HOLD + void (*remote_backlight_set_on_button_hold)(int index); #endif }; diff --git a/apps/plugins/lib/helper.c b/apps/plugins/lib/helper.c index 8c5bc06e8a..393df469a2 100644 --- a/apps/plugins/lib/helper.c +++ b/apps/plugins/lib/helper.c @@ -56,6 +56,7 @@ void backlight_use_settings(void) rb->backlight_set_timeout_plugged(rb->global_settings-> backlight_timeout_plugged); #endif /* CONFIG_CHARGING */ + rb->backlight_set_on_button_hold(rb->global_settings->backlight_on_button_hold); } #else /* HAVE_BACKLIGHT */ /* DUMMY FUNCTIONS */ @@ -113,6 +114,9 @@ void remote_backlight_use_settings(void) rb->remote_backlight_set_timeout_plugged(rb->global_settings-> remote_backlight_timeout_plugged); #endif /* CONFIG_CHARGING */ +#ifdef HAS_REMOTE_BUTTON_HOLD + rb->remote_backlight_set_on_button_hold(rb->global_settings->remote_backlight_on_button_hold); +#endif } #else /* HAVE_REMOTE_LCD */ /* DUMMY FUNCTIONS */ From 2507be9af2603320fd6da395ad399257eb75dafb Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Sun, 13 Sep 2026 12:48:55 -0400 Subject: [PATCH 169/408] FixRed for devices without backlight Change-Id: I202180b529d32b04fad48813af905e9250d3a29b --- apps/plugin.c | 2 ++ apps/plugin.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/apps/plugin.c b/apps/plugin.c index e8723b3aa1..313ee761c9 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -886,7 +886,9 @@ static const struct plugin_api rockbox_api = { #ifdef HAVE_HW_H264 &target_hw_h264_api, #endif +#ifdef HAVE_BACKLIGHT backlight_set_on_button_hold, +#endif #ifdef HAS_REMOTE_BUTTON_HOLD remote_backlight_set_on_button_hold, #endif diff --git a/apps/plugin.h b/apps/plugin.h index 3590786006..9e5c4d66c4 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -1052,7 +1052,9 @@ struct plugin_api { #ifdef HAVE_HW_H264 const struct hw_h264_api *hw_h264; #endif +#ifdef HAVE_BACKLIGHT void (*backlight_set_on_button_hold)(int index); +#endif #ifdef HAS_REMOTE_BUTTON_HOLD void (*remote_backlight_set_on_button_hold)(int index); #endif From 1784c9b8a7f1f2c8c535d8cb9dc965db0fe29e29 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Sun, 13 Sep 2026 13:30:59 -0400 Subject: [PATCH 170/408] FixRed devices with non backlit remotes Change-Id: I42b9c438a5b0571397c5410cfecebe035d4f134e --- apps/plugin.c | 2 +- apps/plugin.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/plugin.c b/apps/plugin.c index 313ee761c9..7f517fe727 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -889,7 +889,7 @@ static const struct plugin_api rockbox_api = { #ifdef HAVE_BACKLIGHT backlight_set_on_button_hold, #endif -#ifdef HAS_REMOTE_BUTTON_HOLD +#if defined(HAVE_REMOTE_LCD) && defined(HAS_REMOTE_BUTTON_HOLD) remote_backlight_set_on_button_hold, #endif }; diff --git a/apps/plugin.h b/apps/plugin.h index 9e5c4d66c4..0cbf638444 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -1055,7 +1055,7 @@ struct plugin_api { #ifdef HAVE_BACKLIGHT void (*backlight_set_on_button_hold)(int index); #endif -#ifdef HAS_REMOTE_BUTTON_HOLD +#if defined(HAVE_REMOTE_LCD) && defined(HAS_REMOTE_BUTTON_HOLD) void (*remote_backlight_set_on_button_hold)(int index); #endif }; From aed1945c5db3c230bb8256dc443fa81049ac78f4 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Sun, 13 Sep 2026 08:36:33 +1000 Subject: [PATCH 171/408] mks5lboot: support the iPod Nano 3G Registers the Nano 3G (platform ipodnano3g, model number 117, "nn3g" header) so mks5lboot can build DFU installers and uninstallers for it, adds its original bootloader to the dualboot code, and lists the platform in the usage text and the README. The target has to be named ipodnano3g rather than nano3g: the dualboot Makefile derives the source directory and the piezo driver's file name from it. The per-target OF hash table is the substantive change. identify_fw() decrypts the IM3 header's data_sign with the hardware UKEY and looks it up in of_sha[], and anything not listed is taken to be a Rockbox bootloader. The table held only iPod Classic firmware, so on a Nano 3G the installer took Apple's own bootloader for a Rockbox one and gave up, and the uninstaller would have refused to restore it. Both bail out before writing, so nothing is damaged, but neither can work. The table is now per target, and lists the bootloader of every Nano 3G firmware release, 1.0.1 to 1.1.3. The decrypted data_sign is the first 16 bytes of the SHA-1 of the plaintext bootloader, so it is the same on every unit. Each release's updater image (aupd, GID-encrypted, in the ipsw) carries that bootloader, 0x1f800 bytes, at the start of a NOR image. The aupd of each release was decrypted on a Nano 3G with the hardware GID key and hashed on the host. For 1.1.3 the result matches the data_sign read from a 4GB unit (model MA978) whose NOR had never been written to, and the bootloader in its aupd is byte-identical to the decrypted copy the installer relocated on that unit. 1.1.2 and 1.1.3 ship the same bootloader. dualboot.c is generated, and only the Nano 3G arrays are added. The iPod Classic arrays are left byte-for-byte as they were: rebuilding them with a different compiler changes their bytes, which would ship an untested installer to Classic users for no reason. The dualboot Makefile did not build from the current tree for any target, which the committed blobs, older than both problems, had hidden. config.h needs autoconf.h, which tools/configure generates per target, so each target now takes a CONFIGDIR_ pointing at a configured bootloader build for it, e.g. make CONFIGDIR_ipod6g=../../../build-ipod6g-bl \ CONFIGDIR_ipodnano3g=../../../build-nano3g-bl And the linker script is preprocessed with __ASSEMBLER__ defined, under which config.h now emits the ldmpc/ldrpc assembler macros that ld rejects; the sed that cleans it now also drops .macro, .endm and .syntax lines and the macro bodies. Before these fixes the iPod Classic build failed first on the missing autoconf.h and then with a linker syntax error; with them it builds both blobs. Tested on that unit, with s5l8702pwnage delivering the images through Apple's DFU, and again with mks5lboot's own --bl-inst and --bl-uninst: the installer put Rockbox in NOR, the unit then booted Rockbox, and holding MENU booted Apple's firmware from the relocated original bootloader; the uninstaller restored it and the unit booted Apple's firmware again. Those images carried a table holding only the 1.1.3 entry. The Nano 3G blobs in dualboot.c were then rebuilt with the Makefile for the full table - with the one-entry table the rebuild was byte-identical to what the tested images carried - and mks5lboot --bl-inst with them installed Rockbox on the same unit, which booted Rockbox and, holding MENU, Apple's firmware. The uninstaller built with the full table has not been run, and no firmware other than 1.1.3 has been installed to or uninstalled from on hardware. For the iPod Classic, the uninstaller DFU this builds is byte-identical to the one built before this change. AI provenance: developed with Claude Opus 5 (Anthropic), used through Claude Code. The model wrote most of the code and this message under Andrew Rice's direction. Any hardware testing described above was carried out by Andrew Rice, who is responsible for this change. Change-Id: I4b2fa692ac4110192ccbde0f1790b0ae2d1c73f5 --- utils/mks5lboot/README | 16 +- utils/mks5lboot/dualboot.c | 635 ++++++++++++++++++++++++++++ utils/mks5lboot/dualboot.h | 2 + utils/mks5lboot/dualboot/Makefile | 20 +- utils/mks5lboot/dualboot/dualboot.c | 23 +- utils/mks5lboot/main.c | 3 +- utils/mks5lboot/mkdfu.c | 4 + utils/mks5lboot/mks5lboot.h | 1 + 8 files changed, 695 insertions(+), 9 deletions(-) diff --git a/utils/mks5lboot/README b/utils/mks5lboot/README index c424f7e617..97c0b81b5a 100644 --- a/utils/mks5lboot/README +++ b/utils/mks5lboot/README @@ -5,7 +5,13 @@ A tool to install/uninstall a dual bootloader into a s5l8702 based device: - iPod Classic 6G - - iPod Nano 3G (WIP) + - iPod Nano 3G + +Nano 3G support is currently limited to 4GB units with the tested Hynix +A514D3AD NAND configuration. The installer cannot identify the NAND while +the device is in DFU mode; see the Rockbox manual's Nano 3G installation +section before installing. Other configurations are rejected by the +bootloader rather than enabled with untested geometry or programming modes. Usage @@ -45,7 +51,8 @@ Commands: is the name of the platform (type of device) for which the DFU uninstaller will be built. Currently supported platform names are: - ipod6g: iPod Classic 6G + ipod6g: iPod Classic 6G + ipodnano3g: iPod Nano 3G Options: -p, --pid Use a specific (Product Id) USB device, @@ -64,13 +71,14 @@ Dual bootloader installation Prerequisites: - - An iPod Classic 6th with Apple firmware installed and running, current - supported FW versions for existing models: + - An iPod Classic 6th or iPod Nano 3G with Apple firmware installed and + running, current supported FW versions for existing models: Classic 6th 80/160 Late 2007 (1G): 1.1.2 Classic 6th 120 Late 2008 (2G): 2.0.1 Classic 6th 160 Late 2009 (3G): 2.0.4 Classic 6th 160 Late 2012 (4G): 2.0.5 + Nano 3G 4GB: 1.0.1 to 1.1.3 - If your iPod is formated using Apple partitions you must convert this ipod to FAT32 format (aka a "winpod"), see http://www.rockbox.org/ diff --git a/utils/mks5lboot/dualboot.c b/utils/mks5lboot/dualboot.c index 4c1608fc41..dbebe29bf9 100644 --- a/utils/mks5lboot/dualboot.c +++ b/utils/mks5lboot/dualboot.c @@ -342,6 +342,333 @@ unsigned char dualboot_install_ipod6g[5396] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; +unsigned char dualboot_install_ipodnano3g[5188] = { + 0xd7, 0x00, 0xa0, 0xe3, 0x00, 0xf0, 0x21, 0xe1, 0x24, 0xd0, 0x9f, 0xe5, 0xdb, 0x00, 0xa0, 0xe3, + 0x00, 0xf0, 0x21, 0xe1, 0x18, 0xd0, 0x9f, 0xe5, 0xd3, 0x00, 0xa0, 0xe3, 0x00, 0xf0, 0x21, 0xe1, + 0x10, 0xd0, 0x9f, 0xe5, 0x05, 0x08, 0xa0, 0xe3, 0x78, 0x00, 0x80, 0xe3, 0x10, 0x0f, 0x01, 0xee, + 0x1d, 0x00, 0x00, 0xeb, 0x0c, 0x03, 0x00, 0x22, 0x0c, 0x03, 0x00, 0x22, 0x30, 0x40, 0x2d, 0xe9, + 0x5c, 0x10, 0x9f, 0xe5, 0x14, 0xd0, 0x4d, 0xe2, 0x10, 0x20, 0xa0, 0xe3, 0x0d, 0x00, 0xa0, 0xe1, + 0xb0, 0x04, 0x00, 0xeb, 0x10, 0x30, 0xa0, 0xe3, 0x0d, 0x20, 0xa0, 0xe1, 0x02, 0x10, 0xa0, 0xe3, + 0x00, 0x00, 0xa0, 0xe3, 0xaf, 0x03, 0x00, 0xeb, 0x38, 0x50, 0x9f, 0xe5, 0x00, 0x40, 0xa0, 0xe3, + 0x10, 0x20, 0xa0, 0xe3, 0x05, 0x10, 0xa0, 0xe1, 0x0d, 0x00, 0xa0, 0xe1, 0x72, 0x00, 0x00, 0xeb, + 0x00, 0x00, 0x50, 0xe3, 0x03, 0x00, 0x00, 0x0a, 0x01, 0x40, 0x84, 0xe2, 0x05, 0x00, 0x54, 0xe3, + 0x10, 0x50, 0x85, 0xe2, 0xf5, 0xff, 0xff, 0x1a, 0x04, 0x00, 0xa0, 0xe1, 0x14, 0xd0, 0x8d, 0xe2, + 0x30, 0x80, 0xbd, 0xe8, 0x10, 0x00, 0x02, 0x22, 0x54, 0x16, 0x00, 0x22, 0x70, 0x40, 0x2d, 0xe9, + 0xc3, 0x00, 0x00, 0xeb, 0x68, 0x41, 0x9f, 0xe5, 0x68, 0x01, 0x9f, 0xe5, 0x83, 0x04, 0x00, 0xeb, + 0x04, 0x10, 0xa0, 0xe3, 0x00, 0x00, 0xa0, 0xe3, 0xe8, 0x00, 0x00, 0xeb, 0x0d, 0x30, 0xd4, 0xe5, + 0x0c, 0x20, 0xd4, 0xe5, 0x0f, 0x10, 0xd4, 0xe5, 0x03, 0x24, 0x82, 0xe1, 0x0e, 0x30, 0xd4, 0xe5, + 0x40, 0x50, 0xd4, 0xe5, 0x03, 0x28, 0x82, 0xe1, 0x01, 0x2c, 0x82, 0xe1, 0x10, 0x30, 0x84, 0xe2, + 0x02, 0x1b, 0x84, 0xe2, 0x02, 0x00, 0xa0, 0xe3, 0x22, 0x04, 0x00, 0xeb, 0x40, 0x30, 0x84, 0xe2, + 0x40, 0x20, 0xa0, 0xe3, 0x04, 0x10, 0xa0, 0xe1, 0x02, 0x00, 0xa0, 0xe3, 0x1d, 0x04, 0x00, 0xeb, + 0x00, 0x00, 0x55, 0xe3, 0x0f, 0x00, 0x00, 0x0a, 0x04, 0x10, 0xa0, 0xe1, 0x02, 0x09, 0xa0, 0xe3, + 0x48, 0x03, 0x00, 0xeb, 0x00, 0x00, 0x50, 0xe3, 0x3b, 0x00, 0x00, 0x1a, 0xf8, 0x00, 0x9f, 0xe5, + 0xf8, 0x10, 0x9f, 0xe5, 0xf8, 0x30, 0x9f, 0xe5, 0xb4, 0x20, 0x91, 0xe5, 0x03, 0x00, 0x52, 0xe1, + 0xfc, 0xff, 0xff, 0x3a, 0x61, 0x04, 0x00, 0xeb, 0xf2, 0x35, 0xa0, 0xe3, 0x01, 0x26, 0xa0, 0xe3, + 0x00, 0x20, 0x83, 0xe5, 0xfe, 0xff, 0xff, 0xea, 0xd8, 0x20, 0x9f, 0xe5, 0xd8, 0x10, 0x9f, 0xe5, + 0x02, 0x09, 0xa0, 0xe3, 0xe5, 0x02, 0x00, 0xeb, 0x00, 0x00, 0x50, 0xe3, 0xee, 0xff, 0xff, 0x1a, + 0xb1, 0xff, 0xff, 0xeb, 0x05, 0x00, 0x50, 0xe3, 0x16, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x9f, 0xe5, + 0xd6, 0x02, 0x00, 0xeb, 0x00, 0x40, 0xa0, 0xe1, 0x46, 0x03, 0x00, 0xeb, 0x04, 0x00, 0x50, 0xe1, + 0xa8, 0x00, 0x9f, 0x35, 0xe5, 0xff, 0xff, 0x3a, 0x9c, 0x10, 0x9f, 0xe5, 0x02, 0x09, 0x84, 0xe2, + 0x28, 0x03, 0x00, 0xeb, 0x00, 0x00, 0x50, 0xe3, 0x16, 0x00, 0x00, 0x1a, 0x84, 0x20, 0x9f, 0xe5, + 0x84, 0x10, 0x9f, 0xe5, 0x01, 0x00, 0xa0, 0xe3, 0x05, 0x04, 0x00, 0xeb, 0x78, 0x10, 0x9f, 0xe5, + 0x02, 0x09, 0xa0, 0xe3, 0x1f, 0x03, 0x00, 0xeb, 0x00, 0x00, 0x50, 0xe3, 0x70, 0x00, 0x9f, 0x05, + 0xd6, 0xff, 0xff, 0x0a, 0xd4, 0xff, 0xff, 0xea, 0x5c, 0x00, 0x9f, 0xe5, 0xbf, 0x02, 0x00, 0xeb, + 0x50, 0x20, 0x9f, 0xe5, 0x50, 0x10, 0x9f, 0xe5, 0x02, 0x09, 0x80, 0xe2, 0xc3, 0x02, 0x00, 0xeb, + 0x00, 0x00, 0x50, 0xe3, 0xcc, 0xff, 0xff, 0x1a, 0x8f, 0xff, 0xff, 0xeb, 0x05, 0x00, 0x50, 0xe3, + 0xdd, 0xff, 0xff, 0x1a, 0xc8, 0xff, 0xff, 0xea, 0x14, 0x10, 0x9f, 0xe5, 0x02, 0x09, 0xa0, 0xe3, + 0x0c, 0x03, 0x00, 0xeb, 0x00, 0x00, 0x50, 0xe3, 0xe3, 0xff, 0xff, 0x0a, 0x24, 0x00, 0x9f, 0xe5, + 0xc2, 0xff, 0xff, 0xea, 0x00, 0x17, 0x00, 0x22, 0xc6, 0x16, 0x00, 0x22, 0xb0, 0x16, 0x00, 0x22, + 0x00, 0x00, 0x70, 0x3c, 0x80, 0x84, 0x1e, 0x00, 0x00, 0x08, 0x02, 0x22, 0x00, 0x00, 0x02, 0x22, + 0xaa, 0x16, 0x00, 0x22, 0xa4, 0x16, 0x00, 0x22, 0xb8, 0x16, 0x00, 0x22, 0x01, 0x10, 0x41, 0xe2, + 0x02, 0x20, 0x80, 0xe0, 0x02, 0x00, 0x50, 0xe1, 0x01, 0x00, 0x00, 0x1a, 0x00, 0x00, 0xa0, 0xe3, + 0x1e, 0xff, 0x2f, 0xe1, 0x00, 0x30, 0xd0, 0xe5, 0x01, 0xc0, 0xf1, 0xe5, 0x0c, 0x00, 0x53, 0xe1, + 0x01, 0x00, 0x00, 0x0a, 0x0c, 0x00, 0x43, 0xe0, 0x1e, 0xff, 0x2f, 0xe1, 0x01, 0x00, 0x80, 0xe2, + 0xf3, 0xff, 0xff, 0xea, 0xc0, 0x32, 0xa0, 0xe1, 0x01, 0x20, 0xa0, 0xe3, 0x1f, 0x00, 0x00, 0xe2, + 0x00, 0x00, 0x51, 0xe3, 0x12, 0x20, 0xa0, 0xe1, 0x01, 0x30, 0x43, 0xe2, 0x0b, 0x00, 0x00, 0x0a, + 0x03, 0x00, 0x53, 0xe3, 0x84, 0x10, 0x9f, 0x95, 0x84, 0x10, 0x9f, 0x85, 0x03, 0x11, 0x91, 0x97, + 0x03, 0x00, 0x53, 0xe3, 0x7c, 0x00, 0x9f, 0x95, 0x00, 0x10, 0x91, 0xe5, 0x03, 0x01, 0x90, 0x97, + 0x6c, 0x00, 0x9f, 0x85, 0x02, 0x30, 0xc1, 0xe1, 0x00, 0x30, 0x80, 0xe5, 0x1e, 0xff, 0x2f, 0xe1, + 0x64, 0x00, 0x9f, 0xe5, 0x64, 0x10, 0x9f, 0xe5, 0x03, 0x00, 0x53, 0xe3, 0x10, 0x00, 0x00, 0x8a, + 0x03, 0x30, 0xd0, 0xe7, 0x03, 0xf1, 0x8f, 0xe0, 0x00, 0x00, 0xa0, 0xe1, 0x6c, 0x10, 0x91, 0xe5, + 0x4c, 0x30, 0x9f, 0xe5, 0x01, 0x20, 0x82, 0xe1, 0x00, 0x20, 0x83, 0xe5, 0x1e, 0xff, 0x2f, 0xe1, + 0x68, 0x10, 0x91, 0xe5, 0x3c, 0x30, 0x9f, 0xe5, 0xf9, 0xff, 0xff, 0xea, 0x58, 0x10, 0x91, 0xe5, + 0x34, 0x30, 0x9f, 0xe5, 0xf6, 0xff, 0xff, 0xea, 0x4c, 0x10, 0x91, 0xe5, 0x2c, 0x30, 0x9f, 0xe5, + 0xf3, 0xff, 0xff, 0xea, 0x48, 0x10, 0x91, 0xe5, 0x04, 0x30, 0x9f, 0xe5, 0xf0, 0xff, 0xff, 0xea, + 0xe0, 0x16, 0x00, 0x22, 0x48, 0x00, 0x50, 0x3c, 0xe0, 0x16, 0x00, 0x22, 0xdc, 0x16, 0x00, 0x22, + 0x00, 0x00, 0x50, 0x3c, 0x6c, 0x00, 0x50, 0x3c, 0x68, 0x00, 0x50, 0x3c, 0x58, 0x00, 0x50, 0x3c, + 0x4c, 0x00, 0x50, 0x3c, 0x04, 0xe0, 0x2d, 0xe5, 0x04, 0xc0, 0x9d, 0xe5, 0x01, 0x10, 0x21, 0xe2, + 0x01, 0xc0, 0x4c, 0xe2, 0x0c, 0xc2, 0xa0, 0xe1, 0xff, 0xc0, 0x0c, 0xe2, 0x02, 0x26, 0xa0, 0xe1, + 0x81, 0xc7, 0x8c, 0xe1, 0x03, 0x2a, 0x02, 0xe2, 0x01, 0x30, 0x43, 0xe2, 0x02, 0xc0, 0x8c, 0xe1, + 0x0f, 0x30, 0x03, 0xe2, 0x03, 0xc0, 0x8c, 0xe1, 0x0c, 0x38, 0xa0, 0xe1, 0x03, 0xc0, 0xc0, 0xe3, + 0x00, 0x20, 0x9c, 0xe5, 0x24, 0xe0, 0x9f, 0xe5, 0x80, 0x11, 0xa0, 0xe1, 0x10, 0x10, 0x01, 0xe2, + 0x3e, 0x21, 0x02, 0xe0, 0x23, 0x38, 0xa0, 0xe1, 0x13, 0x21, 0x82, 0xe1, 0x00, 0x20, 0x8c, 0xe5, + 0xb0, 0x20, 0xd0, 0xe1, 0x03, 0x00, 0x52, 0xe1, 0xfc, 0xff, 0xff, 0x1a, 0x04, 0xf0, 0x9d, 0xe4, + 0x00, 0x00, 0xff, 0xff, 0x60, 0x20, 0x9f, 0xe5, 0x13, 0x40, 0x2d, 0xe9, 0x44, 0x30, 0x92, 0xe5, + 0x01, 0x40, 0xa0, 0xe3, 0x01, 0x3c, 0xc3, 0xe3, 0x44, 0x30, 0x82, 0xe5, 0x04, 0x10, 0xa0, 0xe1, + 0x04, 0x30, 0xa0, 0xe1, 0x00, 0x20, 0xa0, 0xe3, 0x40, 0x00, 0x9f, 0xe5, 0x00, 0x40, 0x8d, 0xe5, + 0xd7, 0xff, 0xff, 0xeb, 0x04, 0x10, 0xa0, 0xe1, 0x25, 0x00, 0xa0, 0xe3, 0xa0, 0xff, 0xff, 0xeb, + 0x2c, 0x30, 0x9f, 0xe5, 0x11, 0x2d, 0xa0, 0xe3, 0xa0, 0x20, 0x83, 0xe5, 0x0b, 0x20, 0xa0, 0xe3, + 0xb0, 0x20, 0x83, 0xe5, 0x00, 0x20, 0xe0, 0xe3, 0xa8, 0x20, 0x83, 0xe5, 0x03, 0x20, 0xa0, 0xe3, + 0xa4, 0x20, 0x83, 0xe5, 0x08, 0xd0, 0x8d, 0xe2, 0x10, 0x80, 0xbd, 0xe8, 0x00, 0x00, 0x50, 0x3c, + 0x12, 0x00, 0x50, 0x3c, 0x00, 0x00, 0x70, 0x3c, 0x24, 0x20, 0x9f, 0xe5, 0x02, 0x00, 0x50, 0xe3, + 0x20, 0x30, 0x9f, 0x05, 0x02, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x50, 0xe3, 0x18, 0x30, 0x9f, 0xe5, + 0x02, 0x30, 0xa0, 0x01, 0x00, 0x30, 0x93, 0xe5, 0x3e, 0x0c, 0x13, 0xe3, 0xf6, 0xff, 0xff, 0x0a, + 0x1e, 0xff, 0x2f, 0xe1, 0x08, 0x00, 0xe0, 0x3c, 0x08, 0x00, 0x20, 0x3d, 0x08, 0x00, 0x30, 0x3c, + 0x04, 0x30, 0x9f, 0xe5, 0x00, 0x11, 0x83, 0xe7, 0x1e, 0xff, 0x2f, 0xe1, 0xd0, 0x16, 0x00, 0x22, + 0x7c, 0x30, 0x9f, 0xe5, 0x00, 0x00, 0x51, 0xe3, 0x78, 0x20, 0x9f, 0xe5, 0x03, 0x20, 0xa0, 0x11, + 0x01, 0x00, 0x50, 0xe3, 0x09, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x50, 0xe3, 0x0e, 0x00, 0x00, 0x0a, + 0x00, 0x00, 0x50, 0xe3, 0x1e, 0xff, 0x2f, 0x11, 0x5c, 0x10, 0x9f, 0xe5, 0x00, 0x30, 0x91, 0xe5, + 0x23, 0x38, 0xa0, 0xe1, 0x03, 0x38, 0x82, 0xe1, 0x00, 0x30, 0x81, 0xe5, 0x1e, 0xff, 0x2f, 0xe1, + 0x44, 0x10, 0x9f, 0xe5, 0xc0, 0x30, 0x91, 0xe5, 0x03, 0x38, 0xa0, 0xe1, 0x23, 0x38, 0xa0, 0xe1, + 0x02, 0x38, 0x83, 0xe1, 0xc0, 0x30, 0x81, 0xe5, 0x1e, 0xff, 0x2f, 0xe1, 0x28, 0x10, 0x9f, 0xe5, + 0xc0, 0x31, 0x91, 0xe5, 0xff, 0x34, 0xc3, 0xe3, 0x02, 0x3c, 0x83, 0xe1, 0xc0, 0x31, 0x81, 0xe5, + 0xe0, 0x31, 0x91, 0xe5, 0xff, 0x30, 0xc3, 0xe3, 0x22, 0x34, 0x83, 0xe1, 0xe0, 0x31, 0x81, 0xe5, + 0x1e, 0xff, 0x2f, 0xe1, 0x22, 0x22, 0x00, 0x00, 0xef, 0xee, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3c, + 0x01, 0x00, 0x50, 0xe3, 0x01, 0x10, 0x21, 0xe2, 0x06, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x50, 0xe3, + 0x09, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x50, 0xe3, 0x24, 0x30, 0x9f, 0x05, 0x0e, 0x10, 0x81, 0x03, + 0x00, 0x12, 0x83, 0x05, 0x1e, 0xff, 0x2f, 0xe1, 0x18, 0x30, 0x9f, 0xe5, 0x10, 0x20, 0x9f, 0xe5, + 0x03, 0x30, 0x81, 0xe1, 0x00, 0x32, 0x82, 0xe5, 0x1e, 0xff, 0x2f, 0xe1, 0x08, 0x30, 0x9f, 0xe5, + 0xf9, 0xff, 0xff, 0xea, 0x00, 0x00, 0xf0, 0x3c, 0x0e, 0x04, 0x06, 0x00, 0x0e, 0x06, 0x0e, 0x00, + 0x02, 0x00, 0x50, 0xe3, 0x10, 0x40, 0x2d, 0xe9, 0x00, 0x40, 0xa0, 0xe1, 0x30, 0x00, 0xa0, 0x03, + 0x02, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x54, 0xe3, 0x22, 0x00, 0xa0, 0x13, 0x2b, 0x00, 0xa0, 0x03, + 0x01, 0x10, 0xa0, 0xe3, 0x3e, 0xff, 0xff, 0xeb, 0x02, 0x00, 0x54, 0xe3, 0x98, 0x30, 0x9f, 0x05, + 0x0f, 0x20, 0xa0, 0xe3, 0x02, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x54, 0xe3, 0x8c, 0x30, 0x9f, 0x05, + 0x8c, 0x30, 0x9f, 0x15, 0x08, 0x20, 0x83, 0xe5, 0x00, 0x20, 0x93, 0xe5, 0x02, 0x00, 0x54, 0xe3, + 0x0c, 0x20, 0x82, 0xe3, 0x00, 0x20, 0x83, 0xe5, 0x78, 0x30, 0x9f, 0xe5, 0x78, 0x20, 0x9f, 0xe5, + 0x04, 0x11, 0x93, 0xe7, 0x11, 0x00, 0x00, 0x1a, 0x5c, 0x30, 0x9f, 0xe5, 0x30, 0x10, 0x83, 0xe5, + 0x06, 0x10, 0xa0, 0xe3, 0x0c, 0x10, 0x83, 0xe5, 0x04, 0x20, 0x83, 0xe5, 0x00, 0x20, 0x93, 0xe5, + 0x02, 0x00, 0x54, 0xe3, 0x0c, 0x20, 0x82, 0xe3, 0x00, 0x20, 0x83, 0xe5, 0x38, 0x30, 0x9f, 0x05, + 0x03, 0x00, 0x00, 0x0a, 0x34, 0x20, 0x9f, 0xe5, 0x01, 0x00, 0x54, 0xe3, 0x30, 0x30, 0x9f, 0xe5, + 0x02, 0x30, 0xa0, 0x01, 0x01, 0x20, 0xa0, 0xe3, 0x00, 0x20, 0x83, 0xe5, 0x10, 0x80, 0xbd, 0xe8, + 0x01, 0x00, 0x54, 0xe3, 0x14, 0x30, 0x9f, 0x05, 0x14, 0x30, 0x9f, 0x15, 0x06, 0x00, 0xa0, 0xe3, + 0x30, 0x10, 0x83, 0xe5, 0x0c, 0x00, 0x83, 0xe5, 0xea, 0xff, 0xff, 0xea, 0x00, 0x00, 0x20, 0x3d, + 0x00, 0x00, 0xe0, 0x3c, 0x00, 0x00, 0x30, 0x3c, 0xd0, 0x16, 0x00, 0x22, 0x18, 0x06, 0x01, 0x00, + 0x02, 0x00, 0x50, 0xe3, 0x30, 0x00, 0xa0, 0x03, 0x02, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x50, 0xe3, + 0x22, 0x00, 0xa0, 0x13, 0x2b, 0x00, 0xa0, 0x03, 0x00, 0x10, 0xa0, 0xe3, 0x08, 0xff, 0xff, 0xea, + 0x02, 0x00, 0x50, 0xe3, 0x90, 0x30, 0x9f, 0x05, 0x10, 0x40, 0x2d, 0xe9, 0x03, 0x00, 0x00, 0x0a, + 0x88, 0x20, 0x9f, 0xe5, 0x01, 0x00, 0x50, 0xe3, 0x84, 0x30, 0x9f, 0xe5, 0x02, 0x30, 0xa0, 0x01, + 0x01, 0x20, 0xa0, 0xe3, 0x00, 0x20, 0x83, 0xe5, 0x78, 0x20, 0x9f, 0xe5, 0x02, 0x00, 0x50, 0xe3, + 0x74, 0x30, 0x9f, 0x05, 0x02, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x50, 0xe3, 0x6c, 0x30, 0x9f, 0xe5, + 0x02, 0x30, 0xa0, 0x01, 0x00, 0x30, 0x93, 0xe5, 0x1f, 0x3e, 0x03, 0xe2, 0x01, 0x0c, 0x53, 0xe3, + 0xf5, 0xff, 0xff, 0x0a, 0x02, 0x00, 0x50, 0xe3, 0x54, 0x30, 0x9f, 0x05, 0x03, 0x00, 0x00, 0x0a, + 0x50, 0x20, 0x9f, 0xe5, 0x01, 0x00, 0x50, 0xe3, 0x4c, 0x30, 0x9f, 0xe5, 0x02, 0x30, 0xa0, 0x01, + 0x00, 0x10, 0x83, 0xe5, 0x57, 0xff, 0xff, 0xeb, 0x02, 0x00, 0x50, 0xe3, 0x3c, 0x30, 0x9f, 0x05, + 0x03, 0x00, 0x00, 0x0a, 0x38, 0x20, 0x9f, 0xe5, 0x01, 0x00, 0x50, 0xe3, 0x34, 0x30, 0x9f, 0xe5, + 0x02, 0x30, 0xa0, 0x01, 0x00, 0x00, 0x93, 0xe5, 0x10, 0x80, 0xbd, 0xe8, 0x34, 0x00, 0x20, 0x3d, + 0x34, 0x00, 0xe0, 0x3c, 0x34, 0x00, 0x30, 0x3c, 0x08, 0x00, 0xe0, 0x3c, 0x08, 0x00, 0x20, 0x3d, + 0x08, 0x00, 0x30, 0x3c, 0x10, 0x00, 0x20, 0x3d, 0x10, 0x00, 0xe0, 0x3c, 0x10, 0x00, 0x30, 0x3c, + 0x20, 0x00, 0x20, 0x3d, 0x20, 0x00, 0xe0, 0x3c, 0x20, 0x00, 0x30, 0x3c, 0x02, 0x00, 0x50, 0xe3, + 0x70, 0x40, 0x2d, 0xe9, 0x02, 0x40, 0xa0, 0xe1, 0x11, 0x00, 0x00, 0x1a, 0xb0, 0x30, 0x9f, 0xe5, + 0xb0, 0x20, 0x9f, 0xe5, 0x34, 0x10, 0x83, 0xe5, 0x04, 0x30, 0x93, 0xe5, 0xa8, 0x50, 0x9f, 0xe5, + 0x01, 0x30, 0x83, 0xe3, 0x01, 0x10, 0x84, 0xe0, 0x00, 0x30, 0x82, 0xe5, 0x01, 0x00, 0x54, 0xe1, + 0x11, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x50, 0xe3, 0x19, 0x00, 0x00, 0x1a, 0x80, 0x30, 0x9f, 0xe5, + 0x80, 0x20, 0x9f, 0xe5, 0x04, 0x30, 0x93, 0xe5, 0x01, 0x30, 0xc3, 0xe3, 0x00, 0x30, 0x82, 0xe5, + 0x70, 0x80, 0xbd, 0xe8, 0x01, 0x00, 0x50, 0xe3, 0x70, 0x30, 0x9f, 0x05, 0x70, 0x30, 0x9f, 0x15, + 0x34, 0x10, 0x83, 0x05, 0x34, 0x10, 0x83, 0x15, 0x04, 0x30, 0x93, 0x05, 0x64, 0x20, 0x9f, 0x05, + 0x04, 0x30, 0x93, 0x15, 0x60, 0x20, 0x9f, 0x15, 0xe7, 0xff, 0xff, 0xea, 0x21, 0xff, 0xff, 0xeb, + 0x02, 0x00, 0x50, 0xe3, 0x54, 0x30, 0x9f, 0x05, 0x02, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x50, 0xe3, + 0x4c, 0x30, 0x9f, 0xe5, 0x05, 0x30, 0xa0, 0x01, 0x00, 0x30, 0x93, 0xe5, 0x01, 0x30, 0xc4, 0xe4, + 0xe1, 0xff, 0xff, 0xea, 0x01, 0x00, 0x50, 0xe3, 0x20, 0x30, 0x9f, 0x05, 0x20, 0x30, 0x9f, 0x15, + 0x04, 0x30, 0x93, 0x05, 0x1c, 0x20, 0x9f, 0x05, 0x04, 0x30, 0x93, 0x15, 0x18, 0x20, 0x9f, 0x15, + 0xe0, 0xff, 0xff, 0xea, 0x00, 0x00, 0x20, 0x3d, 0x04, 0x00, 0x20, 0x3d, 0x20, 0x00, 0xe0, 0x3c, + 0x00, 0x00, 0xe0, 0x3c, 0x00, 0x00, 0x30, 0x3c, 0x04, 0x00, 0xe0, 0x3c, 0x04, 0x00, 0x30, 0x3c, + 0x20, 0x00, 0x20, 0x3d, 0x20, 0x00, 0x30, 0x3c, 0x01, 0x20, 0xd0, 0xe5, 0x00, 0x30, 0xd0, 0xe5, + 0x02, 0x34, 0x83, 0xe1, 0x02, 0x20, 0xd0, 0xe5, 0x03, 0x00, 0xd0, 0xe5, 0x02, 0x38, 0x83, 0xe1, + 0x00, 0x0c, 0x83, 0xe1, 0x1e, 0xff, 0x2f, 0xe1, 0x10, 0x40, 0x2d, 0xe9, 0x00, 0x40, 0xa0, 0xe1, + 0x01, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x30, 0xff, 0xff, 0xeb, 0x05, 0x10, 0xa0, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0x81, 0xff, 0xff, 0xeb, 0xff, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, + 0x7e, 0xff, 0xff, 0xeb, 0x01, 0x10, 0x10, 0xe2, 0x03, 0x00, 0x00, 0x0a, 0x00, 0x10, 0xa0, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0x25, 0xff, 0xff, 0xeb, 0xf0, 0xff, 0xff, 0xea, 0x04, 0x00, 0xa0, 0xe1, + 0x10, 0x40, 0xbd, 0xe8, 0x21, 0xff, 0xff, 0xea, 0x70, 0x40, 0x2d, 0xe9, 0x00, 0x50, 0x51, 0xe2, + 0x00, 0x40, 0xa0, 0xe1, 0x07, 0x00, 0x00, 0x1a, 0x01, 0x10, 0xa0, 0xe3, 0x1b, 0xff, 0xff, 0xeb, + 0x04, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x6c, 0xff, 0xff, 0xeb, 0x05, 0x10, 0xa0, 0xe1, + 0x04, 0x00, 0xa0, 0xe1, 0x15, 0xff, 0xff, 0xeb, 0x01, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, + 0x12, 0xff, 0xff, 0xeb, 0x50, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x63, 0xff, 0xff, 0xeb, + 0x00, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x0c, 0xff, 0xff, 0xeb, 0x01, 0x10, 0xa0, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0x09, 0xff, 0xff, 0xeb, 0x01, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, + 0x5a, 0xff, 0xff, 0xeb, 0x00, 0x00, 0x55, 0xe3, 0x1c, 0x10, 0xa0, 0x03, 0x00, 0x10, 0xa0, 0x13, + 0x04, 0x00, 0xa0, 0xe1, 0x55, 0xff, 0xff, 0xeb, 0x00, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, + 0xfe, 0xfe, 0xff, 0xeb, 0x00, 0x00, 0x55, 0xe3, 0x70, 0x80, 0xbd, 0x08, 0x04, 0x00, 0xa0, 0xe1, + 0x01, 0x10, 0xa0, 0xe3, 0xf9, 0xfe, 0xff, 0xeb, 0x06, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, + 0x4a, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x00, 0x10, 0xa0, 0xe3, 0x70, 0x40, 0xbd, 0xe8, + 0xf2, 0xfe, 0xff, 0xea, 0x70, 0x40, 0x2d, 0xe9, 0x00, 0x40, 0xa0, 0xe1, 0x01, 0x50, 0xa0, 0xe1, + 0x02, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0xb6, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, + 0x01, 0x10, 0xa0, 0xe3, 0xc7, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x01, 0x10, 0xa0, 0xe3, + 0xe6, 0xfe, 0xff, 0xeb, 0x20, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x37, 0xff, 0xff, 0xeb, + 0x25, 0x18, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, 0x04, 0x00, 0xa0, 0xe1, 0x33, 0xff, 0xff, 0xeb, + 0x25, 0x14, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, 0x04, 0x00, 0xa0, 0xe1, 0x2f, 0xff, 0xff, 0xeb, + 0x00, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x2c, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, + 0x00, 0x10, 0xa0, 0xe3, 0xd5, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x00, 0x10, 0xa0, 0xe3, + 0xb0, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x70, 0x40, 0xbd, 0xe8, 0x1b, 0xff, 0xff, 0xea, + 0x10, 0x40, 0x2d, 0xe9, 0x01, 0x10, 0xa0, 0xe3, 0x00, 0x40, 0xa0, 0xe1, 0xa7, 0xfe, 0xff, 0xeb, + 0x04, 0x00, 0xa0, 0xe1, 0x00, 0x10, 0xa0, 0xe3, 0x10, 0x40, 0xbd, 0xe8, 0xc7, 0xfe, 0xff, 0xea, + 0x00, 0x10, 0xa0, 0xe3, 0xa1, 0xfe, 0xff, 0xea, 0xf0, 0x41, 0x2d, 0xe9, 0x00, 0x40, 0xa0, 0xe1, + 0x03, 0x70, 0xa0, 0xe1, 0x02, 0x60, 0xa0, 0xe1, 0x01, 0x50, 0xa0, 0xe1, 0xd3, 0xfe, 0xff, 0xeb, + 0x04, 0x00, 0xa0, 0xe1, 0x87, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x01, 0x10, 0xa0, 0xe3, + 0xba, 0xfe, 0xff, 0xeb, 0x03, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x0b, 0xff, 0xff, 0xeb, + 0x25, 0x18, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, 0x04, 0x00, 0xa0, 0xe1, 0x07, 0xff, 0xff, 0xeb, + 0x25, 0x14, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, 0x04, 0x00, 0xa0, 0xe1, 0x03, 0xff, 0xff, 0xeb, + 0xff, 0x10, 0x05, 0xe2, 0x04, 0x00, 0xa0, 0xe1, 0x00, 0xff, 0xff, 0xeb, 0x07, 0x20, 0xa0, 0xe1, + 0x06, 0x10, 0xa0, 0xe1, 0x04, 0x00, 0xa0, 0xe1, 0x2f, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, + 0x00, 0x10, 0xa0, 0xe3, 0xa5, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0xf0, 0x41, 0xbd, 0xe8, + 0xee, 0xfe, 0xff, 0xea, 0xf0, 0x47, 0x2d, 0xe9, 0x00, 0x40, 0xa0, 0xe1, 0x20, 0xd0, 0x4d, 0xe2, + 0x01, 0x60, 0xa0, 0xe1, 0x03, 0x50, 0xa0, 0xe1, 0x03, 0x70, 0x82, 0xe0, 0xaf, 0xfe, 0xff, 0xeb, + 0x04, 0x00, 0xa0, 0xe1, 0x63, 0xff, 0xff, 0xeb, 0x01, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, + 0x96, 0xfe, 0xff, 0xeb, 0x03, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0xe7, 0xfe, 0xff, 0xeb, + 0x46, 0x18, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, 0x04, 0x00, 0xa0, 0xe1, 0xe3, 0xfe, 0xff, 0xeb, + 0x46, 0x14, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, 0x04, 0x00, 0xa0, 0xe1, 0xdf, 0xfe, 0xff, 0xeb, + 0xff, 0x10, 0x06, 0xe2, 0x04, 0x00, 0xa0, 0xe1, 0xdc, 0xfe, 0xff, 0xeb, 0x00, 0x60, 0xa0, 0xe3, + 0x00, 0x00, 0x55, 0xe3, 0x07, 0x00, 0x00, 0xca, 0x00, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, + 0x82, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0xcc, 0xfe, 0xff, 0xeb, 0x06, 0x00, 0xa0, 0xe1, + 0x20, 0xd0, 0x8d, 0xe2, 0xf0, 0x87, 0xbd, 0xe8, 0x20, 0x00, 0x55, 0xe3, 0x05, 0x90, 0xa0, 0xb1, + 0x20, 0x90, 0xa0, 0xa3, 0x0d, 0x20, 0xa0, 0xe1, 0x09, 0x10, 0xa0, 0xe1, 0x04, 0x00, 0xa0, 0xe1, + 0xfd, 0xfe, 0xff, 0xeb, 0x09, 0x20, 0xa0, 0xe1, 0x0d, 0x10, 0xa0, 0xe1, 0x05, 0x00, 0x47, 0xe0, + 0xc1, 0xfd, 0xff, 0xeb, 0x00, 0x30, 0xa0, 0xe3, 0x00, 0x00, 0x50, 0xe3, 0x01, 0x60, 0x86, 0x13, + 0x03, 0x20, 0xdd, 0xe7, 0x02, 0x30, 0x83, 0xe2, 0xff, 0x00, 0x52, 0xe3, 0x02, 0x60, 0x86, 0x13, + 0x03, 0x00, 0x59, 0xe1, 0xf9, 0xff, 0xff, 0xca, 0x20, 0x50, 0x45, 0xe2, 0xdf, 0xff, 0xff, 0xea, + 0xf7, 0x4f, 0x2d, 0xe9, 0x00, 0x40, 0xa0, 0xe1, 0x01, 0x70, 0xa0, 0xe1, 0x02, 0x50, 0xa0, 0xe1, + 0x03, 0x80, 0xa0, 0xe1, 0x00, 0x00, 0x58, 0xe3, 0x01, 0x00, 0x00, 0xca, 0x0c, 0xd0, 0x8d, 0xe2, + 0xf0, 0x8f, 0xbd, 0xe8, 0x07, 0x6a, 0xa0, 0xe1, 0x26, 0x6a, 0xa0, 0xe1, 0x01, 0x6a, 0x66, 0xe2, + 0x08, 0x00, 0x56, 0xe1, 0x08, 0x60, 0xa0, 0xa1, 0x06, 0x30, 0xa0, 0xe1, 0x05, 0x20, 0xa0, 0xe1, + 0x07, 0x10, 0xa0, 0xe1, 0x04, 0x00, 0xa0, 0xe1, 0xb1, 0xff, 0xff, 0xeb, 0x06, 0xa0, 0x85, 0xe0, + 0x01, 0x00, 0x10, 0xe3, 0x0d, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x10, 0xe3, 0x03, 0x00, 0x00, 0x0a, + 0xff, 0x1e, 0xc7, 0xe3, 0x0f, 0x10, 0xc1, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x58, 0xff, 0xff, 0xeb, + 0x06, 0x90, 0xa0, 0xe1, 0x05, 0x30, 0xa0, 0xe1, 0x00, 0x20, 0xd3, 0xe5, 0xff, 0x00, 0x52, 0xe3, + 0x06, 0x00, 0x00, 0x1a, 0x01, 0x30, 0x83, 0xe2, 0x0a, 0x00, 0x53, 0xe1, 0xf9, 0xff, 0xff, 0x1a, + 0x0a, 0x50, 0xa0, 0xe1, 0x06, 0x70, 0x87, 0xe0, 0x06, 0x80, 0x48, 0xe0, 0xdc, 0xff, 0xff, 0xea, + 0x04, 0x00, 0xa0, 0xe1, 0x51, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x05, 0xff, 0xff, 0xeb, + 0x27, 0xb8, 0xa0, 0xe1, 0x01, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x15, 0xff, 0xff, 0xeb, + 0x02, 0x50, 0x85, 0xe2, 0x01, 0x30, 0xa0, 0xe3, 0xff, 0xb0, 0x0b, 0xe2, 0x01, 0x10, 0xa0, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0x04, 0x30, 0x8d, 0xe5, 0x30, 0xfe, 0xff, 0xeb, 0xad, 0x10, 0xa0, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0x81, 0xfe, 0xff, 0xeb, 0x04, 0x30, 0x9d, 0xe5, 0x00, 0x00, 0x53, 0xe3, + 0x09, 0x00, 0x00, 0x0a, 0x0b, 0x10, 0xa0, 0xe1, 0x04, 0x00, 0xa0, 0xe1, 0x7b, 0xfe, 0xff, 0xeb, + 0x27, 0x14, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, 0x04, 0x00, 0xa0, 0xe1, 0x77, 0xfe, 0xff, 0xeb, + 0xff, 0x10, 0x07, 0xe2, 0x04, 0x00, 0xa0, 0xe1, 0x74, 0xfe, 0xff, 0xeb, 0x02, 0x10, 0x55, 0xe5, + 0x04, 0x00, 0xa0, 0xe1, 0x71, 0xfe, 0xff, 0xeb, 0x01, 0x10, 0x55, 0xe5, 0x04, 0x00, 0xa0, 0xe1, + 0x6e, 0xfe, 0xff, 0xeb, 0x00, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x17, 0xfe, 0xff, 0xeb, + 0x04, 0x00, 0xa0, 0xe1, 0xdf, 0xfe, 0xff, 0xeb, 0x02, 0x90, 0x59, 0xe2, 0x02, 0x50, 0x85, 0xe2, + 0x00, 0x30, 0xa0, 0xe3, 0xdc, 0xff, 0xff, 0x1a, 0x04, 0x00, 0xa0, 0xe1, 0x09, 0x10, 0xa0, 0xe1, + 0xec, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x58, 0xfe, 0xff, 0xeb, 0xc7, 0xff, 0xff, 0xea, + 0x10, 0x40, 0x2d, 0xe9, 0x0c, 0x00, 0x80, 0xe2, 0xca, 0xfe, 0xff, 0xeb, 0x5f, 0x0d, 0x80, 0xe2, + 0x3f, 0x00, 0x80, 0xe2, 0xff, 0x0e, 0xc0, 0xe3, 0x0f, 0x00, 0xc0, 0xe3, 0x10, 0x80, 0xbd, 0xe8, + 0xff, 0x41, 0x2d, 0xe9, 0x01, 0x50, 0xa0, 0xe1, 0x00, 0x80, 0xa0, 0xe1, 0x00, 0x00, 0xa0, 0xe3, + 0x02, 0x60, 0xa0, 0xe1, 0x2d, 0xff, 0xff, 0xeb, 0x05, 0x30, 0xa0, 0xe1, 0x02, 0x2b, 0xa0, 0xe3, + 0x08, 0x10, 0xa0, 0xe1, 0x00, 0x00, 0xa0, 0xe3, 0x32, 0xff, 0xff, 0xeb, 0x00, 0x00, 0xa0, 0xe3, + 0x2e, 0xff, 0xff, 0xeb, 0x08, 0x11, 0x9f, 0xe5, 0x04, 0x20, 0xa0, 0xe3, 0x05, 0x00, 0xa0, 0xe1, + 0x41, 0xfd, 0xff, 0xeb, 0x00, 0x00, 0x50, 0xe3, 0x00, 0x40, 0xe0, 0x13, 0x37, 0x00, 0x00, 0x1a, + 0x0d, 0x30, 0xa0, 0xe1, 0x40, 0x20, 0xa0, 0xe3, 0x05, 0x10, 0xa0, 0xe1, 0x02, 0x00, 0xa0, 0xe3, + 0x08, 0x01, 0x00, 0xeb, 0x10, 0x20, 0xa0, 0xe3, 0x40, 0x10, 0x85, 0xe2, 0x0d, 0x00, 0xa0, 0xe1, + 0x35, 0xfd, 0xff, 0xeb, 0x00, 0x40, 0x50, 0xe2, 0x01, 0x40, 0xe0, 0x13, 0x2b, 0x00, 0x00, 0x1a, + 0x0c, 0x00, 0x85, 0xe2, 0xa3, 0xfe, 0xff, 0xeb, 0x7e, 0x0b, 0x50, 0xe3, 0x00, 0x70, 0xa0, 0xe1, + 0x29, 0x00, 0x00, 0x8a, 0x08, 0x00, 0x85, 0xe2, 0x9e, 0xfe, 0xff, 0xeb, 0x07, 0x00, 0x50, 0xe1, + 0x25, 0x00, 0x00, 0x8a, 0x07, 0x30, 0xd5, 0xe5, 0x01, 0x30, 0x43, 0xe2, 0xff, 0x30, 0x03, 0xe2, + 0x01, 0x00, 0x53, 0xe3, 0x03, 0x40, 0xe0, 0x83, 0x1c, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x56, 0xe3, + 0x1a, 0x00, 0x00, 0x0a, 0x04, 0x00, 0xa0, 0xe1, 0x00, 0xff, 0xff, 0xeb, 0x06, 0x30, 0xa0, 0xe1, + 0x07, 0x20, 0xa0, 0xe1, 0x02, 0x1b, 0x88, 0xe2, 0x04, 0x00, 0xa0, 0xe1, 0x05, 0xff, 0xff, 0xeb, + 0x04, 0x00, 0xa0, 0xe1, 0x01, 0xff, 0xff, 0xeb, 0x07, 0x30, 0xd5, 0xe5, 0x01, 0x00, 0x53, 0xe3, + 0x03, 0x00, 0x00, 0x1a, 0x06, 0x20, 0xa0, 0xe1, 0x05, 0x10, 0xa0, 0xe1, 0x04, 0x00, 0xa0, 0xe1, + 0xf3, 0x00, 0x00, 0xeb, 0x0d, 0x30, 0xa0, 0xe1, 0x07, 0x20, 0xa0, 0xe1, 0x06, 0x10, 0xa0, 0xe1, + 0x02, 0x00, 0xa0, 0xe3, 0xdb, 0x00, 0x00, 0xeb, 0x10, 0x20, 0xa0, 0xe3, 0x02, 0x10, 0x85, 0xe0, + 0x0d, 0x00, 0xa0, 0xe1, 0x08, 0xfd, 0xff, 0xeb, 0x00, 0x00, 0x50, 0xe3, 0x04, 0x40, 0xe0, 0x13, + 0x04, 0x00, 0xa0, 0xe1, 0x10, 0xd0, 0x8d, 0xe2, 0xf0, 0x81, 0xbd, 0xe8, 0x02, 0x40, 0xe0, 0xe3, + 0xfa, 0xff, 0xff, 0xea, 0xf0, 0x16, 0x00, 0x22, 0x70, 0x40, 0x2d, 0xe9, 0x00, 0x60, 0xa0, 0xe1, + 0x0c, 0x00, 0x81, 0xe2, 0x6f, 0xfe, 0xff, 0xeb, 0x01, 0x50, 0xa0, 0xe1, 0x02, 0x4b, 0x80, 0xe2, + 0x00, 0x00, 0xa0, 0xe3, 0xd9, 0xfe, 0xff, 0xeb, 0x04, 0x30, 0xa0, 0xe1, 0x05, 0x20, 0xa0, 0xe1, + 0x06, 0x10, 0xa0, 0xe1, 0x00, 0x00, 0xa0, 0xe3, 0x3c, 0xff, 0xff, 0xeb, 0x04, 0x30, 0xa0, 0xe1, + 0x05, 0x20, 0xa0, 0xe1, 0x06, 0x10, 0xa0, 0xe1, 0x00, 0x00, 0xa0, 0xe3, 0xfc, 0xfe, 0xff, 0xeb, + 0x00, 0x40, 0xa0, 0xe1, 0x00, 0x00, 0xa0, 0xe3, 0xd4, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xe0, 0xe1, + 0x01, 0x00, 0x00, 0xe2, 0x70, 0x80, 0xbd, 0xe8, 0x70, 0x40, 0x2d, 0xe9, 0x68, 0x40, 0x9f, 0xe5, + 0x28, 0xd0, 0x4d, 0xe2, 0x00, 0x00, 0xa0, 0xe3, 0xc4, 0xfe, 0xff, 0xeb, 0x5c, 0x60, 0x9f, 0xe5, + 0x04, 0x50, 0xa0, 0xe1, 0x0d, 0x30, 0xa0, 0xe1, 0x28, 0x20, 0xa0, 0xe3, 0x04, 0x10, 0xa0, 0xe1, + 0x00, 0x00, 0xa0, 0xe3, 0xc7, 0xfe, 0xff, 0xeb, 0x44, 0x10, 0x9f, 0xe5, 0x04, 0x20, 0xa0, 0xe3, + 0x0d, 0x00, 0xa0, 0xe1, 0xd8, 0xfc, 0xff, 0xeb, 0x00, 0x00, 0x50, 0xe3, 0x05, 0x00, 0x00, 0x1a, + 0x0c, 0x30, 0x9d, 0xe5, 0x28, 0x40, 0x84, 0xe2, 0x03, 0x00, 0x55, 0xe1, 0x03, 0x50, 0xa0, 0x21, + 0x06, 0x00, 0x54, 0xe1, 0xee, 0xff, 0xff, 0x1a, 0x00, 0x00, 0xa0, 0xe3, 0xb7, 0xfe, 0xff, 0xeb, + 0x0a, 0x09, 0x45, 0xe2, 0x28, 0xd0, 0x8d, 0xe2, 0x70, 0x80, 0xbd, 0xe8, 0x00, 0xfe, 0x0f, 0x00, + 0xe0, 0xff, 0x0f, 0x00, 0xf5, 0x16, 0x00, 0x22, 0xf0, 0x47, 0x2d, 0xe9, 0x00, 0x80, 0xa0, 0xe1, + 0x01, 0x90, 0xa0, 0xe1, 0x0a, 0x00, 0xa0, 0xe3, 0x01, 0x10, 0xa0, 0xe3, 0xe3, 0x45, 0xa0, 0xe3, + 0x03, 0x50, 0xa0, 0xe1, 0x02, 0x60, 0xa0, 0xe1, 0x01, 0x70, 0xa0, 0xe3, 0xcc, 0xfc, 0xff, 0xeb, + 0x00, 0x00, 0xa0, 0xe3, 0x74, 0x00, 0x84, 0xe5, 0x78, 0x00, 0x84, 0xe5, 0x7c, 0x00, 0x84, 0xe5, + 0x80, 0x00, 0x84, 0xe5, 0x08, 0x70, 0x84, 0xe5, 0x08, 0x00, 0x84, 0xe5, 0x00, 0x70, 0x84, 0xe5, + 0x10, 0x00, 0x84, 0xe5, 0x6c, 0x90, 0x84, 0xe5, 0x6c, 0x30, 0x94, 0xe5, 0x07, 0x00, 0x58, 0xe1, + 0x03, 0x30, 0xe0, 0xe1, 0x88, 0x30, 0x84, 0xe5, 0x08, 0x30, 0xa0, 0x13, 0x09, 0x30, 0xa0, 0x03, + 0x8c, 0x00, 0x84, 0xe5, 0x14, 0x30, 0x84, 0xe5, 0x18, 0x50, 0x84, 0xe5, 0x20, 0x60, 0x84, 0xe5, + 0x24, 0x50, 0x84, 0xe5, 0x28, 0x60, 0x84, 0xe5, 0x2c, 0x50, 0x84, 0xe5, 0x30, 0x60, 0x84, 0xe5, + 0x34, 0x50, 0x84, 0xe5, 0xde, 0x00, 0x00, 0xeb, 0x04, 0x70, 0x84, 0xe5, 0x0c, 0x30, 0x94, 0xe5, + 0x0f, 0x00, 0x13, 0xe3, 0xfc, 0xff, 0xff, 0x0a, 0xf0, 0x47, 0xbd, 0xe8, 0x00, 0x10, 0xa0, 0xe3, + 0x0a, 0x00, 0xa0, 0xe3, 0xaa, 0xfc, 0xff, 0xea, 0xf0, 0x4f, 0x2d, 0xe9, 0x00, 0x60, 0xa0, 0xe1, + 0x54, 0xd0, 0x4d, 0xe2, 0x01, 0x40, 0xa0, 0xe1, 0x00, 0x00, 0xa0, 0xe3, 0x01, 0x10, 0xa0, 0xe3, + 0x02, 0x50, 0xa0, 0xe1, 0xa2, 0xfc, 0xff, 0xeb, 0x0e, 0x23, 0xa0, 0xe3, 0x01, 0x30, 0xa0, 0xe3, + 0x04, 0x30, 0x82, 0xe5, 0x00, 0x30, 0x92, 0xe5, 0x01, 0x30, 0x13, 0xe2, 0xfc, 0xff, 0xff, 0x1a, + 0x04, 0x30, 0x82, 0xe5, 0x00, 0x30, 0x82, 0xe5, 0x40, 0x20, 0x64, 0xe2, 0x06, 0x20, 0x42, 0xe0, + 0x50, 0x71, 0x9f, 0xe5, 0x04, 0x20, 0x8d, 0xe5, 0x04, 0x20, 0x86, 0xe0, 0x08, 0x20, 0x8d, 0xe5, + 0x06, 0x00, 0xa0, 0xe1, 0xa4, 0x2e, 0xa0, 0xe1, 0xa4, 0x9a, 0xa0, 0xe1, 0xa4, 0xa6, 0xa0, 0xe1, + 0x0e, 0x13, 0xa0, 0xe3, 0x0c, 0x20, 0x8d, 0xe5, 0x04, 0x20, 0x9d, 0xe5, 0x00, 0x60, 0x82, 0xe0, + 0x00, 0x00, 0x56, 0xe3, 0x3e, 0x00, 0x00, 0xda, 0x10, 0x20, 0x8d, 0xe2, 0x02, 0xe0, 0xa0, 0xe1, + 0x00, 0xc0, 0xa0, 0xe3, 0x01, 0xc0, 0x8c, 0xe2, 0x10, 0x00, 0x5c, 0xe3, 0x04, 0x30, 0x8e, 0xe4, + 0xfb, 0xff, 0xff, 0x1a, 0x40, 0x00, 0x56, 0xe3, 0x30, 0x00, 0x00, 0xda, 0x0c, 0xc0, 0x9d, 0xe5, + 0x4c, 0x90, 0xcd, 0xe5, 0x4b, 0xc0, 0xcd, 0xe5, 0xa4, 0xc2, 0xa0, 0xe1, 0x4e, 0xc0, 0xcd, 0xe5, + 0x84, 0xc1, 0xa0, 0xe1, 0x4f, 0xc0, 0xcd, 0xe5, 0x01, 0xc0, 0xa0, 0xe3, 0x4d, 0xa0, 0xcd, 0xe5, + 0xd4, 0xe0, 0x9f, 0xe5, 0x04, 0x60, 0x92, 0xe4, 0x04, 0x60, 0x8e, 0xe4, 0x07, 0x00, 0x5e, 0xe1, + 0xfb, 0xff, 0xff, 0x1a, 0x00, 0x20, 0x91, 0xe5, 0x02, 0x20, 0x82, 0xe3, 0x00, 0x20, 0x81, 0xe5, + 0x00, 0x20, 0x91, 0xe5, 0x01, 0x00, 0x12, 0xe3, 0xfc, 0xff, 0xff, 0x1a, 0x00, 0x20, 0x91, 0xe5, + 0x00, 0x00, 0x5c, 0xe3, 0x08, 0x20, 0x82, 0xe3, 0x00, 0x20, 0x81, 0xe5, 0xd9, 0xff, 0xff, 0x0a, + 0x98, 0x30, 0x9f, 0xe5, 0x98, 0x20, 0x9f, 0xe5, 0x04, 0x10, 0x93, 0xe4, 0x02, 0x00, 0x53, 0xe1, + 0x04, 0x10, 0x85, 0xe4, 0xfb, 0xff, 0xff, 0x1a, 0x00, 0x10, 0xa0, 0xe3, 0x01, 0x00, 0xa0, 0xe1, + 0x5f, 0xfc, 0xff, 0xeb, 0x54, 0xd0, 0x8d, 0xe2, 0xf0, 0x8f, 0xbd, 0xe8, 0x0e, 0xb0, 0xd0, 0xe7, + 0x01, 0xe0, 0x8e, 0xe2, 0x01, 0xb0, 0xc8, 0xe4, 0x0e, 0x00, 0x5c, 0xe1, 0xfa, 0xff, 0xff, 0xca, + 0x50, 0xc0, 0x8c, 0xe2, 0x0c, 0xc0, 0x8d, 0xe0, 0x7f, 0xe0, 0xe0, 0xe3, 0x07, 0x00, 0x56, 0xe3, + 0x40, 0xe0, 0x4c, 0xe5, 0xd0, 0xff, 0xff, 0xca, 0x00, 0xc0, 0xa0, 0xe3, 0xd7, 0xff, 0xff, 0xea, + 0x08, 0xc0, 0x9d, 0xe5, 0x02, 0x80, 0xa0, 0xe1, 0x00, 0xe0, 0xa0, 0xe3, 0x00, 0xc0, 0x4c, 0xe0, + 0xf0, 0xff, 0xff, 0xea, 0x20, 0x20, 0x9f, 0xe5, 0x04, 0xc0, 0x40, 0xe2, 0x04, 0xe0, 0xbc, 0xe5, + 0x04, 0xe0, 0x82, 0xe4, 0x07, 0x00, 0x52, 0xe1, 0xfb, 0xff, 0xff, 0x1a, 0x40, 0x00, 0x80, 0xe2, + 0x00, 0xc0, 0xa0, 0xe3, 0xce, 0xff, 0xff, 0xea, 0x80, 0x00, 0x00, 0x38, 0x40, 0x00, 0x00, 0x38, + 0x20, 0x00, 0x00, 0x38, 0x34, 0x00, 0x00, 0x38, 0x30, 0x40, 0x2d, 0xe9, 0x1c, 0xd0, 0x4d, 0xe2, + 0x03, 0x40, 0xa0, 0xe1, 0x00, 0x50, 0xa0, 0xe1, 0x01, 0x00, 0xa0, 0xe1, 0x02, 0x10, 0xa0, 0xe1, + 0x04, 0x20, 0x8d, 0xe2, 0x8b, 0xff, 0xff, 0xeb, 0x04, 0x10, 0x8d, 0xe2, 0x10, 0x20, 0xa0, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0x57, 0x00, 0x00, 0xeb, 0x10, 0x30, 0xa0, 0xe3, 0x04, 0x20, 0xa0, 0xe1, + 0x05, 0x10, 0xa0, 0xe1, 0x01, 0x00, 0xa0, 0xe3, 0x56, 0xff, 0xff, 0xeb, 0x1c, 0xd0, 0x8d, 0xe2, + 0x30, 0x80, 0xbd, 0xe8, 0x70, 0x40, 0x2d, 0xe9, 0x0d, 0x30, 0xd1, 0xe5, 0x0c, 0x40, 0xd1, 0xe5, + 0x01, 0x00, 0x50, 0xe3, 0x03, 0x44, 0x84, 0xe1, 0x0e, 0x30, 0xd1, 0xe5, 0x00, 0x50, 0xa0, 0xe1, + 0x03, 0x48, 0x84, 0xe1, 0x0f, 0x30, 0xd1, 0xe5, 0x02, 0x60, 0xa0, 0xe1, 0x03, 0x4c, 0x84, 0xe1, + 0x02, 0x30, 0xa0, 0x13, 0x01, 0x30, 0xa0, 0x03, 0x07, 0x30, 0xc1, 0xe5, 0x40, 0x20, 0xa0, 0xe3, + 0x40, 0x30, 0x81, 0xe2, 0x02, 0x00, 0xa0, 0xe3, 0xda, 0xff, 0xff, 0xeb, 0x04, 0x30, 0xa0, 0xe1, + 0x06, 0x20, 0xa0, 0xe1, 0x05, 0x00, 0xa0, 0xe1, 0x02, 0x10, 0xa0, 0xe3, 0x70, 0x40, 0xbd, 0xe8, + 0x3c, 0xff, 0xff, 0xea, 0x1c, 0x20, 0x9f, 0xe5, 0x00, 0x00, 0x50, 0xe3, 0x00, 0x30, 0x92, 0xe5, + 0xee, 0x14, 0xa0, 0x03, 0xff, 0x34, 0xc3, 0xe3, 0x53, 0x14, 0xa0, 0x13, 0x01, 0x30, 0x83, 0xe1, + 0x00, 0x30, 0x82, 0xe5, 0x1e, 0xff, 0x2f, 0xe1, 0x00, 0x00, 0xf0, 0x3c, 0x70, 0x40, 0x2d, 0xe9, + 0x60, 0x40, 0x9f, 0xe5, 0xfa, 0x3f, 0xa0, 0xe3, 0xb4, 0x50, 0x94, 0xe5, 0x00, 0x60, 0xa0, 0xe1, + 0x01, 0x00, 0xa0, 0xe3, 0x93, 0x51, 0x25, 0xe0, 0xed, 0xff, 0xff, 0xeb, 0x48, 0x10, 0x9f, 0xe5, + 0x00, 0x30, 0xa0, 0xe3, 0xb4, 0x20, 0x94, 0xe5, 0x05, 0x00, 0x52, 0xe1, 0x02, 0x00, 0x00, 0xba, + 0x70, 0x40, 0xbd, 0xe8, 0x00, 0x00, 0xa0, 0xe3, 0xe5, 0xff, 0xff, 0xea, 0x01, 0x20, 0x23, 0xe2, + 0x06, 0x3c, 0x23, 0xe2, 0x0f, 0x30, 0x23, 0xe2, 0x00, 0x32, 0x81, 0xe5, 0xb4, 0x30, 0x94, 0xe5, + 0xa6, 0x30, 0x83, 0xe0, 0xb4, 0x00, 0x94, 0xe5, 0x03, 0x00, 0x50, 0xe1, 0xfc, 0xff, 0xff, 0xba, + 0x02, 0x30, 0xa0, 0xe1, 0xee, 0xff, 0xff, 0xea, 0x00, 0x00, 0x70, 0x3c, 0x00, 0x00, 0xf0, 0x3c, + 0x70, 0x40, 0x2d, 0xe9, 0x38, 0x50, 0x9f, 0xe5, 0x06, 0x40, 0x80, 0xe2, 0xfa, 0x6f, 0xa0, 0xe3, + 0xb6, 0x00, 0x54, 0xe1, 0x00, 0x00, 0x50, 0xe3, 0x70, 0x80, 0xbd, 0x08, 0xb4, 0x10, 0x54, 0xe1, + 0xd9, 0xff, 0xff, 0xeb, 0xb4, 0x30, 0x95, 0xe5, 0xb2, 0x20, 0x54, 0xe1, 0x96, 0x32, 0x23, 0xe0, + 0xb4, 0x20, 0x95, 0xe5, 0x03, 0x00, 0x52, 0xe1, 0xfc, 0xff, 0xff, 0xba, 0x06, 0x40, 0x84, 0xe2, + 0xf2, 0xff, 0xff, 0xea, 0x00, 0x00, 0x70, 0x3c, 0x01, 0x30, 0x40, 0xe2, 0x02, 0x20, 0x81, 0xe0, + 0x02, 0x00, 0x51, 0xe1, 0x1e, 0xff, 0x2f, 0x01, 0x01, 0xc0, 0xd1, 0xe4, 0x01, 0xc0, 0xe3, 0xe5, + 0xfa, 0xff, 0xff, 0xea, 0x7e, 0xff, 0x17, 0xee, 0xfd, 0xff, 0xff, 0x1a, 0x00, 0x10, 0xa0, 0xe3, + 0x9a, 0x1f, 0x07, 0xee, 0x1e, 0xff, 0x2f, 0xe1, 0xc1, 0x3e, 0xbc, 0x68, 0x7c, 0xcd, 0xe8, 0x22, + 0xad, 0xbd, 0x88, 0x94, 0xa9, 0x95, 0x21, 0x4b, 0xc4, 0x57, 0xfd, 0x04, 0x8e, 0xfb, 0xf1, 0x1a, + 0x9d, 0xfa, 0x80, 0x11, 0x1e, 0x3f, 0xa6, 0x3e, 0x2e, 0x7f, 0xa3, 0x4a, 0xf1, 0xaa, 0x75, 0x4b, + 0x34, 0xf7, 0xb9, 0xc4, 0x58, 0xcf, 0xbd, 0xdb, 0x15, 0xa6, 0x6a, 0xbf, 0xa4, 0x3d, 0xfa, 0x76, + 0x9f, 0xce, 0xce, 0x39, 0x1f, 0x28, 0xeb, 0x47, 0x60, 0xac, 0x5a, 0x12, 0x38, 0x65, 0x0d, 0x2b, + 0xc6, 0x63, 0x15, 0x02, 0xa0, 0x44, 0x84, 0x39, 0xb8, 0x0b, 0xf4, 0x01, 0xf4, 0x01, 0xb8, 0x0b, + 0xf4, 0x01, 0xf4, 0x01, 0xb8, 0x0b, 0xf4, 0x01, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x03, 0x64, 0x00, + 0x00, 0x00, 0xf4, 0x01, 0x96, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf4, 0x01, 0x64, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x0b, 0x08, 0x05, 0x00, 0x4c, 0x00, 0x50, 0x3c, 0x58, 0x00, 0x50, 0x3c, 0x68, 0x00, 0x50, 0x3c, + 0x6c, 0x00, 0x50, 0x3c, 0x38, 0x37, 0x30, 0x32, 0x00, 0x68, 0x73, 0x6c, 0x66, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x38, 0x37, 0x30, 0x32, 0x31, 0x2e, 0x30, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 +}; unsigned char dualboot_uninstall_ipod6g[5076] = { 0xd7, 0x00, 0xa0, 0xe3, 0x00, 0xf0, 0x21, 0xe1, 0x24, 0xd0, 0x9f, 0xe5, 0xdb, 0x00, 0xa0, 0xe3, 0x00, 0xf0, 0x21, 0xe1, 0x18, 0xd0, 0x9f, 0xe5, 0xd3, 0x00, 0xa0, 0xe3, 0x00, 0xf0, 0x21, 0xe1, @@ -662,3 +989,311 @@ unsigned char dualboot_uninstall_ipod6g[5076] = { 0x68, 0x73, 0x6c, 0x66, 0x00, 0x38, 0x37, 0x30, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; +unsigned char dualboot_uninstall_ipodnano3g[4884] = { + 0xd7, 0x00, 0xa0, 0xe3, 0x00, 0xf0, 0x21, 0xe1, 0x24, 0xd0, 0x9f, 0xe5, 0xdb, 0x00, 0xa0, 0xe3, + 0x00, 0xf0, 0x21, 0xe1, 0x18, 0xd0, 0x9f, 0xe5, 0xd3, 0x00, 0xa0, 0xe3, 0x00, 0xf0, 0x21, 0xe1, + 0x10, 0xd0, 0x9f, 0xe5, 0x05, 0x08, 0xa0, 0xe3, 0x78, 0x00, 0x80, 0xe3, 0x10, 0x0f, 0x01, 0xee, + 0x1d, 0x00, 0x00, 0xeb, 0x0c, 0x03, 0x00, 0x22, 0x0c, 0x03, 0x00, 0x22, 0x30, 0x40, 0x2d, 0xe9, + 0x5c, 0x10, 0x9f, 0xe5, 0x14, 0xd0, 0x4d, 0xe2, 0x10, 0x20, 0xa0, 0xe3, 0x0d, 0x00, 0xa0, 0xe1, + 0x7a, 0x04, 0x00, 0xeb, 0x10, 0x30, 0xa0, 0xe3, 0x0d, 0x20, 0xa0, 0xe1, 0x02, 0x10, 0xa0, 0xe3, + 0x00, 0x00, 0xa0, 0xe3, 0x79, 0x03, 0x00, 0xeb, 0x38, 0x50, 0x9f, 0xe5, 0x00, 0x40, 0xa0, 0xe3, + 0x10, 0x20, 0xa0, 0xe3, 0x05, 0x10, 0xa0, 0xe1, 0x0d, 0x00, 0xa0, 0xe1, 0x50, 0x00, 0x00, 0xeb, + 0x00, 0x00, 0x50, 0xe3, 0x03, 0x00, 0x00, 0x0a, 0x01, 0x40, 0x84, 0xe2, 0x05, 0x00, 0x54, 0xe3, + 0x10, 0x50, 0x85, 0xe2, 0xf5, 0xff, 0xff, 0x1a, 0x04, 0x00, 0xa0, 0xe1, 0x14, 0xd0, 0x8d, 0xe2, + 0x30, 0x80, 0xbd, 0xe8, 0x10, 0x00, 0x02, 0x22, 0x7c, 0x15, 0x00, 0x22, 0x70, 0x40, 0x2d, 0xe9, + 0xa1, 0x00, 0x00, 0xeb, 0xe8, 0x00, 0x9f, 0xe5, 0x4e, 0x04, 0x00, 0xeb, 0x04, 0x10, 0xa0, 0xe3, + 0x00, 0x00, 0xa0, 0xe3, 0xc7, 0x00, 0x00, 0xeb, 0xd8, 0x10, 0x9f, 0xe5, 0x00, 0x20, 0xa0, 0xe3, + 0x02, 0x09, 0xa0, 0xe3, 0xf3, 0x02, 0x00, 0xeb, 0x00, 0x00, 0x50, 0xe3, 0x0a, 0x00, 0x00, 0x0a, + 0xc4, 0x00, 0x9f, 0xe5, 0xc4, 0x10, 0x9f, 0xe5, 0xc4, 0x30, 0x9f, 0xe5, 0xb4, 0x20, 0x91, 0xe5, + 0x03, 0x00, 0x52, 0xe1, 0xfc, 0xff, 0xff, 0x3a, 0x3e, 0x04, 0x00, 0xeb, 0xf2, 0x35, 0xa0, 0xe3, + 0x01, 0x26, 0xa0, 0xe3, 0x00, 0x20, 0x83, 0xe5, 0xfe, 0xff, 0xff, 0xea, 0xca, 0xff, 0xff, 0xeb, + 0x05, 0x00, 0x50, 0xe3, 0x20, 0x00, 0x00, 0x1a, 0x88, 0x00, 0x9f, 0xe5, 0xd9, 0x02, 0x00, 0xeb, + 0x90, 0x20, 0x9f, 0xe5, 0x7c, 0x10, 0x9f, 0xe5, 0x00, 0x50, 0xa0, 0xe1, 0x02, 0x09, 0x80, 0xe2, + 0xdc, 0x02, 0x00, 0xeb, 0x00, 0x40, 0x50, 0xe2, 0xe8, 0xff, 0xff, 0x1a, 0xbe, 0xff, 0xff, 0xeb, + 0x05, 0x00, 0x50, 0xe3, 0xe5, 0xff, 0xff, 0x0a, 0x68, 0x20, 0x9f, 0xe5, 0x54, 0x10, 0x9f, 0xe5, + 0x01, 0x00, 0xa0, 0xe3, 0xe8, 0x03, 0x00, 0xeb, 0x48, 0x10, 0x9f, 0xe5, 0x02, 0x09, 0xa0, 0xe3, + 0x22, 0x03, 0x00, 0xeb, 0x00, 0x00, 0x50, 0xe3, 0x4c, 0x00, 0x9f, 0x05, 0xdc, 0xff, 0xff, 0x0a, + 0x04, 0x00, 0xa0, 0xe1, 0xf3, 0x01, 0x00, 0xeb, 0x28, 0x00, 0x9f, 0xe5, 0xc1, 0x02, 0x00, 0xeb, + 0x25, 0x26, 0xa0, 0xe1, 0x02, 0x19, 0x80, 0xe2, 0x21, 0x16, 0xa0, 0xe1, 0x04, 0x00, 0xa0, 0xe1, + 0x19, 0x02, 0x00, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0xf2, 0x01, 0x00, 0xeb, 0x1c, 0x00, 0x9f, 0xe5, + 0xcf, 0xff, 0xff, 0xea, 0xee, 0x15, 0x00, 0x22, 0x00, 0x00, 0x02, 0x22, 0xd8, 0x15, 0x00, 0x22, + 0x00, 0x00, 0x70, 0x3c, 0x80, 0x84, 0x1e, 0x00, 0x00, 0x08, 0x02, 0x22, 0xcc, 0x15, 0x00, 0x22, + 0xe0, 0x15, 0x00, 0x22, 0x01, 0x10, 0x41, 0xe2, 0x02, 0x20, 0x80, 0xe0, 0x02, 0x00, 0x50, 0xe1, + 0x01, 0x00, 0x00, 0x1a, 0x00, 0x00, 0xa0, 0xe3, 0x1e, 0xff, 0x2f, 0xe1, 0x00, 0x30, 0xd0, 0xe5, + 0x01, 0xc0, 0xf1, 0xe5, 0x0c, 0x00, 0x53, 0xe1, 0x01, 0x00, 0x00, 0x0a, 0x0c, 0x00, 0x43, 0xe0, + 0x1e, 0xff, 0x2f, 0xe1, 0x01, 0x00, 0x80, 0xe2, 0xf3, 0xff, 0xff, 0xea, 0xc0, 0x32, 0xa0, 0xe1, + 0x01, 0x20, 0xa0, 0xe3, 0x1f, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x51, 0xe3, 0x12, 0x20, 0xa0, 0xe1, + 0x01, 0x30, 0x43, 0xe2, 0x0b, 0x00, 0x00, 0x0a, 0x03, 0x00, 0x53, 0xe3, 0x84, 0x10, 0x9f, 0x95, + 0x84, 0x10, 0x9f, 0x85, 0x03, 0x11, 0x91, 0x97, 0x03, 0x00, 0x53, 0xe3, 0x7c, 0x00, 0x9f, 0x95, + 0x00, 0x10, 0x91, 0xe5, 0x03, 0x01, 0x90, 0x97, 0x6c, 0x00, 0x9f, 0x85, 0x02, 0x30, 0xc1, 0xe1, + 0x00, 0x30, 0x80, 0xe5, 0x1e, 0xff, 0x2f, 0xe1, 0x64, 0x00, 0x9f, 0xe5, 0x64, 0x10, 0x9f, 0xe5, + 0x03, 0x00, 0x53, 0xe3, 0x10, 0x00, 0x00, 0x8a, 0x03, 0x30, 0xd0, 0xe7, 0x03, 0xf1, 0x8f, 0xe0, + 0x00, 0x00, 0xa0, 0xe1, 0x6c, 0x10, 0x91, 0xe5, 0x4c, 0x30, 0x9f, 0xe5, 0x01, 0x20, 0x82, 0xe1, + 0x00, 0x20, 0x83, 0xe5, 0x1e, 0xff, 0x2f, 0xe1, 0x68, 0x10, 0x91, 0xe5, 0x3c, 0x30, 0x9f, 0xe5, + 0xf9, 0xff, 0xff, 0xea, 0x58, 0x10, 0x91, 0xe5, 0x34, 0x30, 0x9f, 0xe5, 0xf6, 0xff, 0xff, 0xea, + 0x4c, 0x10, 0x91, 0xe5, 0x2c, 0x30, 0x9f, 0xe5, 0xf3, 0xff, 0xff, 0xea, 0x48, 0x10, 0x91, 0xe5, + 0x04, 0x30, 0x9f, 0xe5, 0xf0, 0xff, 0xff, 0xea, 0x08, 0x16, 0x00, 0x22, 0x48, 0x00, 0x50, 0x3c, + 0x08, 0x16, 0x00, 0x22, 0x04, 0x16, 0x00, 0x22, 0x00, 0x00, 0x50, 0x3c, 0x6c, 0x00, 0x50, 0x3c, + 0x68, 0x00, 0x50, 0x3c, 0x58, 0x00, 0x50, 0x3c, 0x4c, 0x00, 0x50, 0x3c, 0x04, 0xe0, 0x2d, 0xe5, + 0x04, 0xc0, 0x9d, 0xe5, 0x01, 0x10, 0x21, 0xe2, 0x01, 0xc0, 0x4c, 0xe2, 0x0c, 0xc2, 0xa0, 0xe1, + 0xff, 0xc0, 0x0c, 0xe2, 0x02, 0x26, 0xa0, 0xe1, 0x81, 0xc7, 0x8c, 0xe1, 0x03, 0x2a, 0x02, 0xe2, + 0x01, 0x30, 0x43, 0xe2, 0x02, 0xc0, 0x8c, 0xe1, 0x0f, 0x30, 0x03, 0xe2, 0x03, 0xc0, 0x8c, 0xe1, + 0x0c, 0x38, 0xa0, 0xe1, 0x03, 0xc0, 0xc0, 0xe3, 0x00, 0x20, 0x9c, 0xe5, 0x24, 0xe0, 0x9f, 0xe5, + 0x80, 0x11, 0xa0, 0xe1, 0x10, 0x10, 0x01, 0xe2, 0x3e, 0x21, 0x02, 0xe0, 0x23, 0x38, 0xa0, 0xe1, + 0x13, 0x21, 0x82, 0xe1, 0x00, 0x20, 0x8c, 0xe5, 0xb0, 0x20, 0xd0, 0xe1, 0x03, 0x00, 0x52, 0xe1, + 0xfc, 0xff, 0xff, 0x1a, 0x04, 0xf0, 0x9d, 0xe4, 0x00, 0x00, 0xff, 0xff, 0x60, 0x20, 0x9f, 0xe5, + 0x13, 0x40, 0x2d, 0xe9, 0x44, 0x30, 0x92, 0xe5, 0x01, 0x40, 0xa0, 0xe3, 0x01, 0x3c, 0xc3, 0xe3, + 0x44, 0x30, 0x82, 0xe5, 0x04, 0x10, 0xa0, 0xe1, 0x04, 0x30, 0xa0, 0xe1, 0x00, 0x20, 0xa0, 0xe3, + 0x40, 0x00, 0x9f, 0xe5, 0x00, 0x40, 0x8d, 0xe5, 0xd7, 0xff, 0xff, 0xeb, 0x04, 0x10, 0xa0, 0xe1, + 0x25, 0x00, 0xa0, 0xe3, 0xa0, 0xff, 0xff, 0xeb, 0x2c, 0x30, 0x9f, 0xe5, 0x11, 0x2d, 0xa0, 0xe3, + 0xa0, 0x20, 0x83, 0xe5, 0x0b, 0x20, 0xa0, 0xe3, 0xb0, 0x20, 0x83, 0xe5, 0x00, 0x20, 0xe0, 0xe3, + 0xa8, 0x20, 0x83, 0xe5, 0x03, 0x20, 0xa0, 0xe3, 0xa4, 0x20, 0x83, 0xe5, 0x08, 0xd0, 0x8d, 0xe2, + 0x10, 0x80, 0xbd, 0xe8, 0x00, 0x00, 0x50, 0x3c, 0x12, 0x00, 0x50, 0x3c, 0x00, 0x00, 0x70, 0x3c, + 0x24, 0x20, 0x9f, 0xe5, 0x02, 0x00, 0x50, 0xe3, 0x20, 0x30, 0x9f, 0x05, 0x02, 0x00, 0x00, 0x0a, + 0x01, 0x00, 0x50, 0xe3, 0x18, 0x30, 0x9f, 0xe5, 0x02, 0x30, 0xa0, 0x01, 0x00, 0x30, 0x93, 0xe5, + 0x3e, 0x0c, 0x13, 0xe3, 0xf6, 0xff, 0xff, 0x0a, 0x1e, 0xff, 0x2f, 0xe1, 0x08, 0x00, 0xe0, 0x3c, + 0x08, 0x00, 0x20, 0x3d, 0x08, 0x00, 0x30, 0x3c, 0x04, 0x30, 0x9f, 0xe5, 0x00, 0x11, 0x83, 0xe7, + 0x1e, 0xff, 0x2f, 0xe1, 0xf8, 0x15, 0x00, 0x22, 0x7c, 0x30, 0x9f, 0xe5, 0x00, 0x00, 0x51, 0xe3, + 0x78, 0x20, 0x9f, 0xe5, 0x03, 0x20, 0xa0, 0x11, 0x01, 0x00, 0x50, 0xe3, 0x09, 0x00, 0x00, 0x0a, + 0x02, 0x00, 0x50, 0xe3, 0x0e, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x50, 0xe3, 0x1e, 0xff, 0x2f, 0x11, + 0x5c, 0x10, 0x9f, 0xe5, 0x00, 0x30, 0x91, 0xe5, 0x23, 0x38, 0xa0, 0xe1, 0x03, 0x38, 0x82, 0xe1, + 0x00, 0x30, 0x81, 0xe5, 0x1e, 0xff, 0x2f, 0xe1, 0x44, 0x10, 0x9f, 0xe5, 0xc0, 0x30, 0x91, 0xe5, + 0x03, 0x38, 0xa0, 0xe1, 0x23, 0x38, 0xa0, 0xe1, 0x02, 0x38, 0x83, 0xe1, 0xc0, 0x30, 0x81, 0xe5, + 0x1e, 0xff, 0x2f, 0xe1, 0x28, 0x10, 0x9f, 0xe5, 0xc0, 0x31, 0x91, 0xe5, 0xff, 0x34, 0xc3, 0xe3, + 0x02, 0x3c, 0x83, 0xe1, 0xc0, 0x31, 0x81, 0xe5, 0xe0, 0x31, 0x91, 0xe5, 0xff, 0x30, 0xc3, 0xe3, + 0x22, 0x34, 0x83, 0xe1, 0xe0, 0x31, 0x81, 0xe5, 0x1e, 0xff, 0x2f, 0xe1, 0x22, 0x22, 0x00, 0x00, + 0xef, 0xee, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3c, 0x01, 0x00, 0x50, 0xe3, 0x01, 0x10, 0x21, 0xe2, + 0x06, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x50, 0xe3, 0x09, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x50, 0xe3, + 0x24, 0x30, 0x9f, 0x05, 0x0e, 0x10, 0x81, 0x03, 0x00, 0x12, 0x83, 0x05, 0x1e, 0xff, 0x2f, 0xe1, + 0x18, 0x30, 0x9f, 0xe5, 0x10, 0x20, 0x9f, 0xe5, 0x03, 0x30, 0x81, 0xe1, 0x00, 0x32, 0x82, 0xe5, + 0x1e, 0xff, 0x2f, 0xe1, 0x08, 0x30, 0x9f, 0xe5, 0xf9, 0xff, 0xff, 0xea, 0x00, 0x00, 0xf0, 0x3c, + 0x0e, 0x04, 0x06, 0x00, 0x0e, 0x06, 0x0e, 0x00, 0x02, 0x00, 0x50, 0xe3, 0x10, 0x40, 0x2d, 0xe9, + 0x00, 0x40, 0xa0, 0xe1, 0x30, 0x00, 0xa0, 0x03, 0x02, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x54, 0xe3, + 0x22, 0x00, 0xa0, 0x13, 0x2b, 0x00, 0xa0, 0x03, 0x01, 0x10, 0xa0, 0xe3, 0x3e, 0xff, 0xff, 0xeb, + 0x02, 0x00, 0x54, 0xe3, 0x98, 0x30, 0x9f, 0x05, 0x0f, 0x20, 0xa0, 0xe3, 0x02, 0x00, 0x00, 0x0a, + 0x01, 0x00, 0x54, 0xe3, 0x8c, 0x30, 0x9f, 0x05, 0x8c, 0x30, 0x9f, 0x15, 0x08, 0x20, 0x83, 0xe5, + 0x00, 0x20, 0x93, 0xe5, 0x02, 0x00, 0x54, 0xe3, 0x0c, 0x20, 0x82, 0xe3, 0x00, 0x20, 0x83, 0xe5, + 0x78, 0x30, 0x9f, 0xe5, 0x78, 0x20, 0x9f, 0xe5, 0x04, 0x11, 0x93, 0xe7, 0x11, 0x00, 0x00, 0x1a, + 0x5c, 0x30, 0x9f, 0xe5, 0x30, 0x10, 0x83, 0xe5, 0x06, 0x10, 0xa0, 0xe3, 0x0c, 0x10, 0x83, 0xe5, + 0x04, 0x20, 0x83, 0xe5, 0x00, 0x20, 0x93, 0xe5, 0x02, 0x00, 0x54, 0xe3, 0x0c, 0x20, 0x82, 0xe3, + 0x00, 0x20, 0x83, 0xe5, 0x38, 0x30, 0x9f, 0x05, 0x03, 0x00, 0x00, 0x0a, 0x34, 0x20, 0x9f, 0xe5, + 0x01, 0x00, 0x54, 0xe3, 0x30, 0x30, 0x9f, 0xe5, 0x02, 0x30, 0xa0, 0x01, 0x01, 0x20, 0xa0, 0xe3, + 0x00, 0x20, 0x83, 0xe5, 0x10, 0x80, 0xbd, 0xe8, 0x01, 0x00, 0x54, 0xe3, 0x14, 0x30, 0x9f, 0x05, + 0x14, 0x30, 0x9f, 0x15, 0x06, 0x00, 0xa0, 0xe3, 0x30, 0x10, 0x83, 0xe5, 0x0c, 0x00, 0x83, 0xe5, + 0xea, 0xff, 0xff, 0xea, 0x00, 0x00, 0x20, 0x3d, 0x00, 0x00, 0xe0, 0x3c, 0x00, 0x00, 0x30, 0x3c, + 0xf8, 0x15, 0x00, 0x22, 0x18, 0x06, 0x01, 0x00, 0x02, 0x00, 0x50, 0xe3, 0x30, 0x00, 0xa0, 0x03, + 0x02, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x50, 0xe3, 0x22, 0x00, 0xa0, 0x13, 0x2b, 0x00, 0xa0, 0x03, + 0x00, 0x10, 0xa0, 0xe3, 0x08, 0xff, 0xff, 0xea, 0x02, 0x00, 0x50, 0xe3, 0x90, 0x30, 0x9f, 0x05, + 0x10, 0x40, 0x2d, 0xe9, 0x03, 0x00, 0x00, 0x0a, 0x88, 0x20, 0x9f, 0xe5, 0x01, 0x00, 0x50, 0xe3, + 0x84, 0x30, 0x9f, 0xe5, 0x02, 0x30, 0xa0, 0x01, 0x01, 0x20, 0xa0, 0xe3, 0x00, 0x20, 0x83, 0xe5, + 0x78, 0x20, 0x9f, 0xe5, 0x02, 0x00, 0x50, 0xe3, 0x74, 0x30, 0x9f, 0x05, 0x02, 0x00, 0x00, 0x0a, + 0x01, 0x00, 0x50, 0xe3, 0x6c, 0x30, 0x9f, 0xe5, 0x02, 0x30, 0xa0, 0x01, 0x00, 0x30, 0x93, 0xe5, + 0x1f, 0x3e, 0x03, 0xe2, 0x01, 0x0c, 0x53, 0xe3, 0xf5, 0xff, 0xff, 0x0a, 0x02, 0x00, 0x50, 0xe3, + 0x54, 0x30, 0x9f, 0x05, 0x03, 0x00, 0x00, 0x0a, 0x50, 0x20, 0x9f, 0xe5, 0x01, 0x00, 0x50, 0xe3, + 0x4c, 0x30, 0x9f, 0xe5, 0x02, 0x30, 0xa0, 0x01, 0x00, 0x10, 0x83, 0xe5, 0x57, 0xff, 0xff, 0xeb, + 0x02, 0x00, 0x50, 0xe3, 0x3c, 0x30, 0x9f, 0x05, 0x03, 0x00, 0x00, 0x0a, 0x38, 0x20, 0x9f, 0xe5, + 0x01, 0x00, 0x50, 0xe3, 0x34, 0x30, 0x9f, 0xe5, 0x02, 0x30, 0xa0, 0x01, 0x00, 0x00, 0x93, 0xe5, + 0x10, 0x80, 0xbd, 0xe8, 0x34, 0x00, 0x20, 0x3d, 0x34, 0x00, 0xe0, 0x3c, 0x34, 0x00, 0x30, 0x3c, + 0x08, 0x00, 0xe0, 0x3c, 0x08, 0x00, 0x20, 0x3d, 0x08, 0x00, 0x30, 0x3c, 0x10, 0x00, 0x20, 0x3d, + 0x10, 0x00, 0xe0, 0x3c, 0x10, 0x00, 0x30, 0x3c, 0x20, 0x00, 0x20, 0x3d, 0x20, 0x00, 0xe0, 0x3c, + 0x20, 0x00, 0x30, 0x3c, 0x02, 0x00, 0x50, 0xe3, 0x70, 0x40, 0x2d, 0xe9, 0x02, 0x40, 0xa0, 0xe1, + 0x11, 0x00, 0x00, 0x1a, 0xb0, 0x30, 0x9f, 0xe5, 0xb0, 0x20, 0x9f, 0xe5, 0x34, 0x10, 0x83, 0xe5, + 0x04, 0x30, 0x93, 0xe5, 0xa8, 0x50, 0x9f, 0xe5, 0x01, 0x30, 0x83, 0xe3, 0x01, 0x10, 0x84, 0xe0, + 0x00, 0x30, 0x82, 0xe5, 0x01, 0x00, 0x54, 0xe1, 0x11, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x50, 0xe3, + 0x19, 0x00, 0x00, 0x1a, 0x80, 0x30, 0x9f, 0xe5, 0x80, 0x20, 0x9f, 0xe5, 0x04, 0x30, 0x93, 0xe5, + 0x01, 0x30, 0xc3, 0xe3, 0x00, 0x30, 0x82, 0xe5, 0x70, 0x80, 0xbd, 0xe8, 0x01, 0x00, 0x50, 0xe3, + 0x70, 0x30, 0x9f, 0x05, 0x70, 0x30, 0x9f, 0x15, 0x34, 0x10, 0x83, 0x05, 0x34, 0x10, 0x83, 0x15, + 0x04, 0x30, 0x93, 0x05, 0x64, 0x20, 0x9f, 0x05, 0x04, 0x30, 0x93, 0x15, 0x60, 0x20, 0x9f, 0x15, + 0xe7, 0xff, 0xff, 0xea, 0x21, 0xff, 0xff, 0xeb, 0x02, 0x00, 0x50, 0xe3, 0x54, 0x30, 0x9f, 0x05, + 0x02, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x50, 0xe3, 0x4c, 0x30, 0x9f, 0xe5, 0x05, 0x30, 0xa0, 0x01, + 0x00, 0x30, 0x93, 0xe5, 0x01, 0x30, 0xc4, 0xe4, 0xe1, 0xff, 0xff, 0xea, 0x01, 0x00, 0x50, 0xe3, + 0x20, 0x30, 0x9f, 0x05, 0x20, 0x30, 0x9f, 0x15, 0x04, 0x30, 0x93, 0x05, 0x1c, 0x20, 0x9f, 0x05, + 0x04, 0x30, 0x93, 0x15, 0x18, 0x20, 0x9f, 0x15, 0xe0, 0xff, 0xff, 0xea, 0x00, 0x00, 0x20, 0x3d, + 0x04, 0x00, 0x20, 0x3d, 0x20, 0x00, 0xe0, 0x3c, 0x00, 0x00, 0xe0, 0x3c, 0x00, 0x00, 0x30, 0x3c, + 0x04, 0x00, 0xe0, 0x3c, 0x04, 0x00, 0x30, 0x3c, 0x20, 0x00, 0x20, 0x3d, 0x20, 0x00, 0x30, 0x3c, + 0x01, 0x20, 0xd0, 0xe5, 0x00, 0x30, 0xd0, 0xe5, 0x02, 0x34, 0x83, 0xe1, 0x02, 0x20, 0xd0, 0xe5, + 0x03, 0x00, 0xd0, 0xe5, 0x02, 0x38, 0x83, 0xe1, 0x00, 0x0c, 0x83, 0xe1, 0x1e, 0xff, 0x2f, 0xe1, + 0x10, 0x40, 0x2d, 0xe9, 0x00, 0x40, 0xa0, 0xe1, 0x01, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, + 0x30, 0xff, 0xff, 0xeb, 0x05, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x81, 0xff, 0xff, 0xeb, + 0xff, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x7e, 0xff, 0xff, 0xeb, 0x01, 0x10, 0x10, 0xe2, + 0x03, 0x00, 0x00, 0x0a, 0x00, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x25, 0xff, 0xff, 0xeb, + 0xf0, 0xff, 0xff, 0xea, 0x04, 0x00, 0xa0, 0xe1, 0x10, 0x40, 0xbd, 0xe8, 0x21, 0xff, 0xff, 0xea, + 0x70, 0x40, 0x2d, 0xe9, 0x00, 0x50, 0x51, 0xe2, 0x00, 0x40, 0xa0, 0xe1, 0x07, 0x00, 0x00, 0x1a, + 0x01, 0x10, 0xa0, 0xe3, 0x1b, 0xff, 0xff, 0xeb, 0x04, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, + 0x6c, 0xff, 0xff, 0xeb, 0x05, 0x10, 0xa0, 0xe1, 0x04, 0x00, 0xa0, 0xe1, 0x15, 0xff, 0xff, 0xeb, + 0x01, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x12, 0xff, 0xff, 0xeb, 0x50, 0x10, 0xa0, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0x63, 0xff, 0xff, 0xeb, 0x00, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, + 0x0c, 0xff, 0xff, 0xeb, 0x01, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x09, 0xff, 0xff, 0xeb, + 0x01, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x5a, 0xff, 0xff, 0xeb, 0x00, 0x00, 0x55, 0xe3, + 0x1c, 0x10, 0xa0, 0x03, 0x00, 0x10, 0xa0, 0x13, 0x04, 0x00, 0xa0, 0xe1, 0x55, 0xff, 0xff, 0xeb, + 0x00, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0xfe, 0xfe, 0xff, 0xeb, 0x00, 0x00, 0x55, 0xe3, + 0x70, 0x80, 0xbd, 0x08, 0x04, 0x00, 0xa0, 0xe1, 0x01, 0x10, 0xa0, 0xe3, 0xf9, 0xfe, 0xff, 0xeb, + 0x06, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x4a, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, + 0x00, 0x10, 0xa0, 0xe3, 0x70, 0x40, 0xbd, 0xe8, 0xf2, 0xfe, 0xff, 0xea, 0x70, 0x40, 0x2d, 0xe9, + 0x00, 0x40, 0xa0, 0xe1, 0x01, 0x50, 0xa0, 0xe1, 0x02, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, + 0xb6, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x01, 0x10, 0xa0, 0xe3, 0xc7, 0xff, 0xff, 0xeb, + 0x04, 0x00, 0xa0, 0xe1, 0x01, 0x10, 0xa0, 0xe3, 0xe6, 0xfe, 0xff, 0xeb, 0x20, 0x10, 0xa0, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0x37, 0xff, 0xff, 0xeb, 0x25, 0x18, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, + 0x04, 0x00, 0xa0, 0xe1, 0x33, 0xff, 0xff, 0xeb, 0x25, 0x14, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, + 0x04, 0x00, 0xa0, 0xe1, 0x2f, 0xff, 0xff, 0xeb, 0x00, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, + 0x2c, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x00, 0x10, 0xa0, 0xe3, 0xd5, 0xfe, 0xff, 0xeb, + 0x04, 0x00, 0xa0, 0xe1, 0x00, 0x10, 0xa0, 0xe3, 0xb0, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, + 0x70, 0x40, 0xbd, 0xe8, 0x1b, 0xff, 0xff, 0xea, 0x10, 0x40, 0x2d, 0xe9, 0x01, 0x10, 0xa0, 0xe3, + 0x00, 0x40, 0xa0, 0xe1, 0xa7, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x00, 0x10, 0xa0, 0xe3, + 0x10, 0x40, 0xbd, 0xe8, 0xc7, 0xfe, 0xff, 0xea, 0x00, 0x10, 0xa0, 0xe3, 0xa1, 0xfe, 0xff, 0xea, + 0xf0, 0x41, 0x2d, 0xe9, 0x00, 0x40, 0xa0, 0xe1, 0x03, 0x70, 0xa0, 0xe1, 0x02, 0x60, 0xa0, 0xe1, + 0x01, 0x50, 0xa0, 0xe1, 0xd3, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x87, 0xff, 0xff, 0xeb, + 0x04, 0x00, 0xa0, 0xe1, 0x01, 0x10, 0xa0, 0xe3, 0xba, 0xfe, 0xff, 0xeb, 0x03, 0x10, 0xa0, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0x0b, 0xff, 0xff, 0xeb, 0x25, 0x18, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, + 0x04, 0x00, 0xa0, 0xe1, 0x07, 0xff, 0xff, 0xeb, 0x25, 0x14, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, + 0x04, 0x00, 0xa0, 0xe1, 0x03, 0xff, 0xff, 0xeb, 0xff, 0x10, 0x05, 0xe2, 0x04, 0x00, 0xa0, 0xe1, + 0x00, 0xff, 0xff, 0xeb, 0x07, 0x20, 0xa0, 0xe1, 0x06, 0x10, 0xa0, 0xe1, 0x04, 0x00, 0xa0, 0xe1, + 0x2f, 0xff, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x00, 0x10, 0xa0, 0xe3, 0xa5, 0xfe, 0xff, 0xeb, + 0x04, 0x00, 0xa0, 0xe1, 0xf0, 0x41, 0xbd, 0xe8, 0xee, 0xfe, 0xff, 0xea, 0x70, 0x40, 0x2d, 0xe9, + 0x00, 0x60, 0xa0, 0xe1, 0x02, 0x40, 0xa0, 0xe1, 0x01, 0x56, 0xa0, 0xe1, 0x00, 0x00, 0x54, 0xe3, + 0x70, 0x80, 0xbd, 0x08, 0x05, 0x10, 0xa0, 0xe1, 0x06, 0x00, 0xa0, 0xe1, 0xa6, 0xff, 0xff, 0xeb, + 0x01, 0x40, 0x44, 0xe2, 0x01, 0x5a, 0x85, 0xe2, 0xf7, 0xff, 0xff, 0xea, 0xf0, 0x47, 0x2d, 0xe9, + 0x00, 0x40, 0xa0, 0xe1, 0x20, 0xd0, 0x4d, 0xe2, 0x01, 0x60, 0xa0, 0xe1, 0x03, 0x50, 0xa0, 0xe1, + 0x03, 0x70, 0x82, 0xe0, 0xa3, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0x57, 0xff, 0xff, 0xeb, + 0x01, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x8a, 0xfe, 0xff, 0xeb, 0x03, 0x10, 0xa0, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0xdb, 0xfe, 0xff, 0xeb, 0x46, 0x18, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, + 0x04, 0x00, 0xa0, 0xe1, 0xd7, 0xfe, 0xff, 0xeb, 0x46, 0x14, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, + 0x04, 0x00, 0xa0, 0xe1, 0xd3, 0xfe, 0xff, 0xeb, 0xff, 0x10, 0x06, 0xe2, 0x04, 0x00, 0xa0, 0xe1, + 0xd0, 0xfe, 0xff, 0xeb, 0x00, 0x60, 0xa0, 0xe3, 0x00, 0x00, 0x55, 0xe3, 0x07, 0x00, 0x00, 0xca, + 0x00, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x76, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, + 0xc0, 0xfe, 0xff, 0xeb, 0x06, 0x00, 0xa0, 0xe1, 0x20, 0xd0, 0x8d, 0xe2, 0xf0, 0x87, 0xbd, 0xe8, + 0x20, 0x00, 0x55, 0xe3, 0x05, 0x90, 0xa0, 0xb1, 0x20, 0x90, 0xa0, 0xa3, 0x0d, 0x20, 0xa0, 0xe1, + 0x09, 0x10, 0xa0, 0xe1, 0x04, 0x00, 0xa0, 0xe1, 0xf1, 0xfe, 0xff, 0xeb, 0x09, 0x20, 0xa0, 0xe1, + 0x0d, 0x10, 0xa0, 0xe1, 0x05, 0x00, 0x47, 0xe0, 0xb5, 0xfd, 0xff, 0xeb, 0x00, 0x30, 0xa0, 0xe3, + 0x00, 0x00, 0x50, 0xe3, 0x01, 0x60, 0x86, 0x13, 0x03, 0x20, 0xdd, 0xe7, 0x02, 0x30, 0x83, 0xe2, + 0xff, 0x00, 0x52, 0xe3, 0x02, 0x60, 0x86, 0x13, 0x03, 0x00, 0x59, 0xe1, 0xf9, 0xff, 0xff, 0xca, + 0x20, 0x50, 0x45, 0xe2, 0xdf, 0xff, 0xff, 0xea, 0xf7, 0x4f, 0x2d, 0xe9, 0x00, 0x40, 0xa0, 0xe1, + 0x01, 0x70, 0xa0, 0xe1, 0x02, 0x50, 0xa0, 0xe1, 0x03, 0x80, 0xa0, 0xe1, 0x00, 0x00, 0x58, 0xe3, + 0x01, 0x00, 0x00, 0xca, 0x0c, 0xd0, 0x8d, 0xe2, 0xf0, 0x8f, 0xbd, 0xe8, 0x07, 0x6a, 0xa0, 0xe1, + 0x26, 0x6a, 0xa0, 0xe1, 0x01, 0x6a, 0x66, 0xe2, 0x08, 0x00, 0x56, 0xe1, 0x08, 0x60, 0xa0, 0xa1, + 0x06, 0x30, 0xa0, 0xe1, 0x05, 0x20, 0xa0, 0xe1, 0x07, 0x10, 0xa0, 0xe1, 0x04, 0x00, 0xa0, 0xe1, + 0xb1, 0xff, 0xff, 0xeb, 0x06, 0xa0, 0x85, 0xe0, 0x01, 0x00, 0x10, 0xe3, 0x0d, 0x00, 0x00, 0x0a, + 0x02, 0x00, 0x10, 0xe3, 0x03, 0x00, 0x00, 0x0a, 0xff, 0x1e, 0xc7, 0xe3, 0x0f, 0x10, 0xc1, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0x4c, 0xff, 0xff, 0xeb, 0x06, 0x90, 0xa0, 0xe1, 0x05, 0x30, 0xa0, 0xe1, + 0x00, 0x20, 0xd3, 0xe5, 0xff, 0x00, 0x52, 0xe3, 0x06, 0x00, 0x00, 0x1a, 0x01, 0x30, 0x83, 0xe2, + 0x0a, 0x00, 0x53, 0xe1, 0xf9, 0xff, 0xff, 0x1a, 0x0a, 0x50, 0xa0, 0xe1, 0x06, 0x70, 0x87, 0xe0, + 0x06, 0x80, 0x48, 0xe0, 0xdc, 0xff, 0xff, 0xea, 0x04, 0x00, 0xa0, 0xe1, 0x45, 0xfe, 0xff, 0xeb, + 0x04, 0x00, 0xa0, 0xe1, 0xf9, 0xfe, 0xff, 0xeb, 0x27, 0xb8, 0xa0, 0xe1, 0x01, 0x10, 0xa0, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0x09, 0xff, 0xff, 0xeb, 0x02, 0x50, 0x85, 0xe2, 0x01, 0x30, 0xa0, 0xe3, + 0xff, 0xb0, 0x0b, 0xe2, 0x01, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x04, 0x30, 0x8d, 0xe5, + 0x24, 0xfe, 0xff, 0xeb, 0xad, 0x10, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x75, 0xfe, 0xff, 0xeb, + 0x04, 0x30, 0x9d, 0xe5, 0x00, 0x00, 0x53, 0xe3, 0x09, 0x00, 0x00, 0x0a, 0x0b, 0x10, 0xa0, 0xe1, + 0x04, 0x00, 0xa0, 0xe1, 0x6f, 0xfe, 0xff, 0xeb, 0x27, 0x14, 0xa0, 0xe1, 0xff, 0x10, 0x01, 0xe2, + 0x04, 0x00, 0xa0, 0xe1, 0x6b, 0xfe, 0xff, 0xeb, 0xff, 0x10, 0x07, 0xe2, 0x04, 0x00, 0xa0, 0xe1, + 0x68, 0xfe, 0xff, 0xeb, 0x02, 0x10, 0x55, 0xe5, 0x04, 0x00, 0xa0, 0xe1, 0x65, 0xfe, 0xff, 0xeb, + 0x01, 0x10, 0x55, 0xe5, 0x04, 0x00, 0xa0, 0xe1, 0x62, 0xfe, 0xff, 0xeb, 0x00, 0x10, 0xa0, 0xe3, + 0x04, 0x00, 0xa0, 0xe1, 0x0b, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0xd3, 0xfe, 0xff, 0xeb, + 0x02, 0x90, 0x59, 0xe2, 0x02, 0x50, 0x85, 0xe2, 0x00, 0x30, 0xa0, 0xe3, 0xdc, 0xff, 0xff, 0x1a, + 0x04, 0x00, 0xa0, 0xe1, 0x09, 0x10, 0xa0, 0xe1, 0xe0, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, + 0x4c, 0xfe, 0xff, 0xeb, 0xc7, 0xff, 0xff, 0xea, 0x10, 0x40, 0x2d, 0xe9, 0x0c, 0x00, 0x80, 0xe2, + 0xbe, 0xfe, 0xff, 0xeb, 0x5f, 0x0d, 0x80, 0xe2, 0x3f, 0x00, 0x80, 0xe2, 0xff, 0x0e, 0xc0, 0xe3, + 0x0f, 0x00, 0xc0, 0xe3, 0x10, 0x80, 0xbd, 0xe8, 0xff, 0x41, 0x2d, 0xe9, 0x01, 0x50, 0xa0, 0xe1, + 0x00, 0x80, 0xa0, 0xe1, 0x00, 0x00, 0xa0, 0xe3, 0x02, 0x60, 0xa0, 0xe1, 0x21, 0xff, 0xff, 0xeb, + 0x05, 0x30, 0xa0, 0xe1, 0x02, 0x2b, 0xa0, 0xe3, 0x08, 0x10, 0xa0, 0xe1, 0x00, 0x00, 0xa0, 0xe3, + 0x26, 0xff, 0xff, 0xeb, 0x00, 0x00, 0xa0, 0xe3, 0x22, 0xff, 0xff, 0xeb, 0x08, 0x11, 0x9f, 0xe5, + 0x04, 0x20, 0xa0, 0xe3, 0x05, 0x00, 0xa0, 0xe1, 0x35, 0xfd, 0xff, 0xeb, 0x00, 0x00, 0x50, 0xe3, + 0x00, 0x40, 0xe0, 0x13, 0x37, 0x00, 0x00, 0x1a, 0x0d, 0x30, 0xa0, 0xe1, 0x40, 0x20, 0xa0, 0xe3, + 0x05, 0x10, 0xa0, 0xe1, 0x02, 0x00, 0xa0, 0xe3, 0xe8, 0x00, 0x00, 0xeb, 0x10, 0x20, 0xa0, 0xe3, + 0x40, 0x10, 0x85, 0xe2, 0x0d, 0x00, 0xa0, 0xe1, 0x29, 0xfd, 0xff, 0xeb, 0x00, 0x40, 0x50, 0xe2, + 0x01, 0x40, 0xe0, 0x13, 0x2b, 0x00, 0x00, 0x1a, 0x0c, 0x00, 0x85, 0xe2, 0x97, 0xfe, 0xff, 0xeb, + 0x7e, 0x0b, 0x50, 0xe3, 0x00, 0x70, 0xa0, 0xe1, 0x29, 0x00, 0x00, 0x8a, 0x08, 0x00, 0x85, 0xe2, + 0x92, 0xfe, 0xff, 0xeb, 0x07, 0x00, 0x50, 0xe1, 0x25, 0x00, 0x00, 0x8a, 0x07, 0x30, 0xd5, 0xe5, + 0x01, 0x30, 0x43, 0xe2, 0xff, 0x30, 0x03, 0xe2, 0x01, 0x00, 0x53, 0xe3, 0x03, 0x40, 0xe0, 0x83, + 0x1c, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x56, 0xe3, 0x1a, 0x00, 0x00, 0x0a, 0x04, 0x00, 0xa0, 0xe1, + 0xf4, 0xfe, 0xff, 0xeb, 0x06, 0x30, 0xa0, 0xe1, 0x07, 0x20, 0xa0, 0xe1, 0x02, 0x1b, 0x88, 0xe2, + 0x04, 0x00, 0xa0, 0xe1, 0xf9, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xa0, 0xe1, 0xf5, 0xfe, 0xff, 0xeb, + 0x07, 0x30, 0xd5, 0xe5, 0x01, 0x00, 0x53, 0xe3, 0x03, 0x00, 0x00, 0x1a, 0x06, 0x20, 0xa0, 0xe1, + 0x05, 0x10, 0xa0, 0xe1, 0x04, 0x00, 0xa0, 0xe1, 0xd3, 0x00, 0x00, 0xeb, 0x0d, 0x30, 0xa0, 0xe1, + 0x07, 0x20, 0xa0, 0xe1, 0x06, 0x10, 0xa0, 0xe1, 0x02, 0x00, 0xa0, 0xe3, 0xbb, 0x00, 0x00, 0xeb, + 0x10, 0x20, 0xa0, 0xe3, 0x02, 0x10, 0x85, 0xe0, 0x0d, 0x00, 0xa0, 0xe1, 0xfc, 0xfc, 0xff, 0xeb, + 0x00, 0x00, 0x50, 0xe3, 0x04, 0x40, 0xe0, 0x13, 0x04, 0x00, 0xa0, 0xe1, 0x10, 0xd0, 0x8d, 0xe2, + 0xf0, 0x81, 0xbd, 0xe8, 0x02, 0x40, 0xe0, 0xe3, 0xfa, 0xff, 0xff, 0xea, 0x18, 0x16, 0x00, 0x22, + 0x70, 0x40, 0x2d, 0xe9, 0x00, 0x60, 0xa0, 0xe1, 0x0c, 0x00, 0x81, 0xe2, 0x63, 0xfe, 0xff, 0xeb, + 0x01, 0x50, 0xa0, 0xe1, 0x02, 0x4b, 0x80, 0xe2, 0x00, 0x00, 0xa0, 0xe3, 0xcd, 0xfe, 0xff, 0xeb, + 0x04, 0x30, 0xa0, 0xe1, 0x05, 0x20, 0xa0, 0xe1, 0x06, 0x10, 0xa0, 0xe1, 0x00, 0x00, 0xa0, 0xe3, + 0x3c, 0xff, 0xff, 0xeb, 0x04, 0x30, 0xa0, 0xe1, 0x05, 0x20, 0xa0, 0xe1, 0x06, 0x10, 0xa0, 0xe1, + 0x00, 0x00, 0xa0, 0xe3, 0xfc, 0xfe, 0xff, 0xeb, 0x00, 0x40, 0xa0, 0xe1, 0x00, 0x00, 0xa0, 0xe3, + 0xc8, 0xfe, 0xff, 0xeb, 0x04, 0x00, 0xe0, 0xe1, 0x01, 0x00, 0x00, 0xe2, 0x70, 0x80, 0xbd, 0xe8, + 0xf0, 0x47, 0x2d, 0xe9, 0x00, 0x80, 0xa0, 0xe1, 0x01, 0x90, 0xa0, 0xe1, 0x0a, 0x00, 0xa0, 0xe3, + 0x01, 0x10, 0xa0, 0xe3, 0xe3, 0x45, 0xa0, 0xe3, 0x03, 0x50, 0xa0, 0xe1, 0x02, 0x60, 0xa0, 0xe1, + 0x01, 0x70, 0xa0, 0xe3, 0xe0, 0xfc, 0xff, 0xeb, 0x00, 0x00, 0xa0, 0xe3, 0x74, 0x00, 0x84, 0xe5, + 0x78, 0x00, 0x84, 0xe5, 0x7c, 0x00, 0x84, 0xe5, 0x80, 0x00, 0x84, 0xe5, 0x08, 0x70, 0x84, 0xe5, + 0x08, 0x00, 0x84, 0xe5, 0x00, 0x70, 0x84, 0xe5, 0x10, 0x00, 0x84, 0xe5, 0x6c, 0x90, 0x84, 0xe5, + 0x6c, 0x30, 0x94, 0xe5, 0x07, 0x00, 0x58, 0xe1, 0x03, 0x30, 0xe0, 0xe1, 0x88, 0x30, 0x84, 0xe5, + 0x08, 0x30, 0xa0, 0x13, 0x09, 0x30, 0xa0, 0x03, 0x8c, 0x00, 0x84, 0xe5, 0x14, 0x30, 0x84, 0xe5, + 0x18, 0x50, 0x84, 0xe5, 0x20, 0x60, 0x84, 0xe5, 0x24, 0x50, 0x84, 0xe5, 0x28, 0x60, 0x84, 0xe5, + 0x2c, 0x50, 0x84, 0xe5, 0x30, 0x60, 0x84, 0xe5, 0x34, 0x50, 0x84, 0xe5, 0xde, 0x00, 0x00, 0xeb, + 0x04, 0x70, 0x84, 0xe5, 0x0c, 0x30, 0x94, 0xe5, 0x0f, 0x00, 0x13, 0xe3, 0xfc, 0xff, 0xff, 0x0a, + 0xf0, 0x47, 0xbd, 0xe8, 0x00, 0x10, 0xa0, 0xe3, 0x0a, 0x00, 0xa0, 0xe3, 0xbe, 0xfc, 0xff, 0xea, + 0xf0, 0x4f, 0x2d, 0xe9, 0x00, 0x60, 0xa0, 0xe1, 0x54, 0xd0, 0x4d, 0xe2, 0x01, 0x40, 0xa0, 0xe1, + 0x00, 0x00, 0xa0, 0xe3, 0x01, 0x10, 0xa0, 0xe3, 0x02, 0x50, 0xa0, 0xe1, 0xb6, 0xfc, 0xff, 0xeb, + 0x0e, 0x23, 0xa0, 0xe3, 0x01, 0x30, 0xa0, 0xe3, 0x04, 0x30, 0x82, 0xe5, 0x00, 0x30, 0x92, 0xe5, + 0x01, 0x30, 0x13, 0xe2, 0xfc, 0xff, 0xff, 0x1a, 0x04, 0x30, 0x82, 0xe5, 0x00, 0x30, 0x82, 0xe5, + 0x40, 0x20, 0x64, 0xe2, 0x06, 0x20, 0x42, 0xe0, 0x50, 0x71, 0x9f, 0xe5, 0x04, 0x20, 0x8d, 0xe5, + 0x04, 0x20, 0x86, 0xe0, 0x08, 0x20, 0x8d, 0xe5, 0x06, 0x00, 0xa0, 0xe1, 0xa4, 0x2e, 0xa0, 0xe1, + 0xa4, 0x9a, 0xa0, 0xe1, 0xa4, 0xa6, 0xa0, 0xe1, 0x0e, 0x13, 0xa0, 0xe3, 0x0c, 0x20, 0x8d, 0xe5, + 0x04, 0x20, 0x9d, 0xe5, 0x00, 0x60, 0x82, 0xe0, 0x00, 0x00, 0x56, 0xe3, 0x3e, 0x00, 0x00, 0xda, + 0x10, 0x20, 0x8d, 0xe2, 0x02, 0xe0, 0xa0, 0xe1, 0x00, 0xc0, 0xa0, 0xe3, 0x01, 0xc0, 0x8c, 0xe2, + 0x10, 0x00, 0x5c, 0xe3, 0x04, 0x30, 0x8e, 0xe4, 0xfb, 0xff, 0xff, 0x1a, 0x40, 0x00, 0x56, 0xe3, + 0x30, 0x00, 0x00, 0xda, 0x0c, 0xc0, 0x9d, 0xe5, 0x4c, 0x90, 0xcd, 0xe5, 0x4b, 0xc0, 0xcd, 0xe5, + 0xa4, 0xc2, 0xa0, 0xe1, 0x4e, 0xc0, 0xcd, 0xe5, 0x84, 0xc1, 0xa0, 0xe1, 0x4f, 0xc0, 0xcd, 0xe5, + 0x01, 0xc0, 0xa0, 0xe3, 0x4d, 0xa0, 0xcd, 0xe5, 0xd4, 0xe0, 0x9f, 0xe5, 0x04, 0x60, 0x92, 0xe4, + 0x04, 0x60, 0x8e, 0xe4, 0x07, 0x00, 0x5e, 0xe1, 0xfb, 0xff, 0xff, 0x1a, 0x00, 0x20, 0x91, 0xe5, + 0x02, 0x20, 0x82, 0xe3, 0x00, 0x20, 0x81, 0xe5, 0x00, 0x20, 0x91, 0xe5, 0x01, 0x00, 0x12, 0xe3, + 0xfc, 0xff, 0xff, 0x1a, 0x00, 0x20, 0x91, 0xe5, 0x00, 0x00, 0x5c, 0xe3, 0x08, 0x20, 0x82, 0xe3, + 0x00, 0x20, 0x81, 0xe5, 0xd9, 0xff, 0xff, 0x0a, 0x98, 0x30, 0x9f, 0xe5, 0x98, 0x20, 0x9f, 0xe5, + 0x04, 0x10, 0x93, 0xe4, 0x02, 0x00, 0x53, 0xe1, 0x04, 0x10, 0x85, 0xe4, 0xfb, 0xff, 0xff, 0x1a, + 0x00, 0x10, 0xa0, 0xe3, 0x01, 0x00, 0xa0, 0xe1, 0x73, 0xfc, 0xff, 0xeb, 0x54, 0xd0, 0x8d, 0xe2, + 0xf0, 0x8f, 0xbd, 0xe8, 0x0e, 0xb0, 0xd0, 0xe7, 0x01, 0xe0, 0x8e, 0xe2, 0x01, 0xb0, 0xc8, 0xe4, + 0x0e, 0x00, 0x5c, 0xe1, 0xfa, 0xff, 0xff, 0xca, 0x50, 0xc0, 0x8c, 0xe2, 0x0c, 0xc0, 0x8d, 0xe0, + 0x7f, 0xe0, 0xe0, 0xe3, 0x07, 0x00, 0x56, 0xe3, 0x40, 0xe0, 0x4c, 0xe5, 0xd0, 0xff, 0xff, 0xca, + 0x00, 0xc0, 0xa0, 0xe3, 0xd7, 0xff, 0xff, 0xea, 0x08, 0xc0, 0x9d, 0xe5, 0x02, 0x80, 0xa0, 0xe1, + 0x00, 0xe0, 0xa0, 0xe3, 0x00, 0xc0, 0x4c, 0xe0, 0xf0, 0xff, 0xff, 0xea, 0x20, 0x20, 0x9f, 0xe5, + 0x04, 0xc0, 0x40, 0xe2, 0x04, 0xe0, 0xbc, 0xe5, 0x04, 0xe0, 0x82, 0xe4, 0x07, 0x00, 0x52, 0xe1, + 0xfb, 0xff, 0xff, 0x1a, 0x40, 0x00, 0x80, 0xe2, 0x00, 0xc0, 0xa0, 0xe3, 0xce, 0xff, 0xff, 0xea, + 0x80, 0x00, 0x00, 0x38, 0x40, 0x00, 0x00, 0x38, 0x20, 0x00, 0x00, 0x38, 0x34, 0x00, 0x00, 0x38, + 0x30, 0x40, 0x2d, 0xe9, 0x1c, 0xd0, 0x4d, 0xe2, 0x03, 0x40, 0xa0, 0xe1, 0x00, 0x50, 0xa0, 0xe1, + 0x01, 0x00, 0xa0, 0xe1, 0x02, 0x10, 0xa0, 0xe1, 0x04, 0x20, 0x8d, 0xe2, 0x8b, 0xff, 0xff, 0xeb, + 0x04, 0x10, 0x8d, 0xe2, 0x10, 0x20, 0xa0, 0xe3, 0x04, 0x00, 0xa0, 0xe1, 0x57, 0x00, 0x00, 0xeb, + 0x10, 0x30, 0xa0, 0xe3, 0x04, 0x20, 0xa0, 0xe1, 0x05, 0x10, 0xa0, 0xe1, 0x01, 0x00, 0xa0, 0xe3, + 0x56, 0xff, 0xff, 0xeb, 0x1c, 0xd0, 0x8d, 0xe2, 0x30, 0x80, 0xbd, 0xe8, 0x70, 0x40, 0x2d, 0xe9, + 0x0d, 0x30, 0xd1, 0xe5, 0x0c, 0x40, 0xd1, 0xe5, 0x01, 0x00, 0x50, 0xe3, 0x03, 0x44, 0x84, 0xe1, + 0x0e, 0x30, 0xd1, 0xe5, 0x00, 0x50, 0xa0, 0xe1, 0x03, 0x48, 0x84, 0xe1, 0x0f, 0x30, 0xd1, 0xe5, + 0x02, 0x60, 0xa0, 0xe1, 0x03, 0x4c, 0x84, 0xe1, 0x02, 0x30, 0xa0, 0x13, 0x01, 0x30, 0xa0, 0x03, + 0x07, 0x30, 0xc1, 0xe5, 0x40, 0x20, 0xa0, 0xe3, 0x40, 0x30, 0x81, 0xe2, 0x02, 0x00, 0xa0, 0xe3, + 0xda, 0xff, 0xff, 0xeb, 0x04, 0x30, 0xa0, 0xe1, 0x06, 0x20, 0xa0, 0xe1, 0x05, 0x00, 0xa0, 0xe1, + 0x02, 0x10, 0xa0, 0xe3, 0x70, 0x40, 0xbd, 0xe8, 0x3c, 0xff, 0xff, 0xea, 0x1c, 0x20, 0x9f, 0xe5, + 0x00, 0x00, 0x50, 0xe3, 0x00, 0x30, 0x92, 0xe5, 0xee, 0x14, 0xa0, 0x03, 0xff, 0x34, 0xc3, 0xe3, + 0x53, 0x14, 0xa0, 0x13, 0x01, 0x30, 0x83, 0xe1, 0x00, 0x30, 0x82, 0xe5, 0x1e, 0xff, 0x2f, 0xe1, + 0x00, 0x00, 0xf0, 0x3c, 0x70, 0x40, 0x2d, 0xe9, 0x60, 0x40, 0x9f, 0xe5, 0xfa, 0x3f, 0xa0, 0xe3, + 0xb4, 0x50, 0x94, 0xe5, 0x00, 0x60, 0xa0, 0xe1, 0x01, 0x00, 0xa0, 0xe3, 0x93, 0x51, 0x25, 0xe0, + 0xed, 0xff, 0xff, 0xeb, 0x48, 0x10, 0x9f, 0xe5, 0x00, 0x30, 0xa0, 0xe3, 0xb4, 0x20, 0x94, 0xe5, + 0x05, 0x00, 0x52, 0xe1, 0x02, 0x00, 0x00, 0xba, 0x70, 0x40, 0xbd, 0xe8, 0x00, 0x00, 0xa0, 0xe3, + 0xe5, 0xff, 0xff, 0xea, 0x01, 0x20, 0x23, 0xe2, 0x06, 0x3c, 0x23, 0xe2, 0x0f, 0x30, 0x23, 0xe2, + 0x00, 0x32, 0x81, 0xe5, 0xb4, 0x30, 0x94, 0xe5, 0xa6, 0x30, 0x83, 0xe0, 0xb4, 0x00, 0x94, 0xe5, + 0x03, 0x00, 0x50, 0xe1, 0xfc, 0xff, 0xff, 0xba, 0x02, 0x30, 0xa0, 0xe1, 0xee, 0xff, 0xff, 0xea, + 0x00, 0x00, 0x70, 0x3c, 0x00, 0x00, 0xf0, 0x3c, 0x70, 0x40, 0x2d, 0xe9, 0x38, 0x50, 0x9f, 0xe5, + 0x06, 0x40, 0x80, 0xe2, 0xfa, 0x6f, 0xa0, 0xe3, 0xb6, 0x00, 0x54, 0xe1, 0x00, 0x00, 0x50, 0xe3, + 0x70, 0x80, 0xbd, 0x08, 0xb4, 0x10, 0x54, 0xe1, 0xd9, 0xff, 0xff, 0xeb, 0xb4, 0x30, 0x95, 0xe5, + 0xb2, 0x20, 0x54, 0xe1, 0x96, 0x32, 0x23, 0xe0, 0xb4, 0x20, 0x95, 0xe5, 0x03, 0x00, 0x52, 0xe1, + 0xfc, 0xff, 0xff, 0xba, 0x06, 0x40, 0x84, 0xe2, 0xf2, 0xff, 0xff, 0xea, 0x00, 0x00, 0x70, 0x3c, + 0x01, 0x30, 0x40, 0xe2, 0x02, 0x20, 0x81, 0xe0, 0x02, 0x00, 0x51, 0xe1, 0x1e, 0xff, 0x2f, 0x01, + 0x01, 0xc0, 0xd1, 0xe4, 0x01, 0xc0, 0xe3, 0xe5, 0xfa, 0xff, 0xff, 0xea, 0x7e, 0xff, 0x17, 0xee, + 0xfd, 0xff, 0xff, 0x1a, 0x00, 0x10, 0xa0, 0xe3, 0x9a, 0x1f, 0x07, 0xee, 0x1e, 0xff, 0x2f, 0xe1, + 0xc1, 0x3e, 0xbc, 0x68, 0x7c, 0xcd, 0xe8, 0x22, 0xad, 0xbd, 0x88, 0x94, 0xa9, 0x95, 0x21, 0x4b, + 0xc4, 0x57, 0xfd, 0x04, 0x8e, 0xfb, 0xf1, 0x1a, 0x9d, 0xfa, 0x80, 0x11, 0x1e, 0x3f, 0xa6, 0x3e, + 0x2e, 0x7f, 0xa3, 0x4a, 0xf1, 0xaa, 0x75, 0x4b, 0x34, 0xf7, 0xb9, 0xc4, 0x58, 0xcf, 0xbd, 0xdb, + 0x15, 0xa6, 0x6a, 0xbf, 0xa4, 0x3d, 0xfa, 0x76, 0x9f, 0xce, 0xce, 0x39, 0x1f, 0x28, 0xeb, 0x47, + 0x60, 0xac, 0x5a, 0x12, 0x38, 0x65, 0x0d, 0x2b, 0xc6, 0x63, 0x15, 0x02, 0xa0, 0x44, 0x84, 0x39, + 0xb8, 0x0b, 0xf4, 0x01, 0xf4, 0x01, 0xb8, 0x0b, 0xf4, 0x01, 0xf4, 0x01, 0xb8, 0x0b, 0xf4, 0x01, + 0x00, 0x00, 0x00, 0x00, 0xe8, 0x03, 0x64, 0x00, 0x00, 0x00, 0xf4, 0x01, 0x96, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf4, 0x01, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0b, 0x08, 0x05, 0x00, 0x4c, 0x00, 0x50, 0x3c, + 0x58, 0x00, 0x50, 0x3c, 0x68, 0x00, 0x50, 0x3c, 0x6c, 0x00, 0x50, 0x3c, 0x38, 0x37, 0x30, 0x32, + 0x00, 0x00, 0x00, 0x00 +}; diff --git a/utils/mks5lboot/dualboot.h b/utils/mks5lboot/dualboot.h index c8952a4c07..969cadedfc 100644 --- a/utils/mks5lboot/dualboot.h +++ b/utils/mks5lboot/dualboot.h @@ -1,4 +1,6 @@ /* Generated by bin2c */ extern unsigned char dualboot_install_ipod6g[5396]; +extern unsigned char dualboot_install_ipodnano3g[5188]; extern unsigned char dualboot_uninstall_ipod6g[5076]; +extern unsigned char dualboot_uninstall_ipodnano3g[4884]; diff --git a/utils/mks5lboot/dualboot/Makefile b/utils/mks5lboot/dualboot/Makefile index ad0603a216..d17916f819 100644 --- a/utils/mks5lboot/dualboot/Makefile +++ b/utils/mks5lboot/dualboot/Makefile @@ -27,8 +27,9 @@ LINKFILE = dualboot.lds # 1) add x to the list in TARGETS # 2) create a variable named OPT_x of the form: # OPT_x=target specific defines -TARGETS = ipod6g +TARGETS = ipod6g ipodnano3g OPT_ipod6g = -DIPOD_6G -DMEMORYSIZE=64 +OPT_ipodnano3g = -DIPOD_NANO3G -DMEMORYSIZE=32 LOADERS = install uninstall OPT_install = @@ -53,7 +54,15 @@ $(foreach l, $(LOADERS), $(foreach t, $(TARGETS),\ INCLUDES += -I. -I.. -I$(FIRMDIR) -I$(FWARM) -I$(FW8702) INCLUDES += $(addprefix -I$(FIRMDIR)/, export include libc/include kernel/include) # target/loader specific includes -$(foreach l,$(LOADERS),$(foreach t,$(TARGETS),$(eval INC_$(l)_$(t) = -I$(FW8702)/$(t)))) +# +# Each target also needs the autoconf.h that tools/configure generates for +# its bootloader build; it cannot be derived here. Configure a bootloader +# build for each target and pass the directories, e.g. +# +# make CONFIGDIR_ipod6g=../../../build-ipod6g-bl \ +# CONFIGDIR_ipodnano3g=../../../build-nano3g-bl +$(foreach l,$(LOADERS),$(foreach t,$(TARGETS),\ + $(eval INC_$(l)_$(t) = -I$(FW8702)/$(t) -I$(CONFIGDIR_$(t))))) CFLAGS = $(INCLUDES) -mcpu=arm926ej-s -std=gnu99 -nostdlib -ffreestanding -Os -W -Wall\ -Wundef -Wstrict-prototypes -ffunction-sections -fdata-sections -Wl,--gc-sections $(DEFINES) @@ -69,9 +78,14 @@ OUTPUTDEBUG = $(BOOTBINS:%.arm-bin=$(BUILDDIR)%.arm-elf) $(BOOTBINS:%.arm-bin=$( all: $(BUILDDIR) $(OUTPUTDUALBOOT) +# config.h emits ARM assembler macros (ldmpc/ldrpc) whenever __ASSEMBLER__ is +# defined, which the linker script has to do; strip them and their bodies, or +# ld chokes on them. Lines containing a backslash are macro bodies. $(BUILDDIR)$(PFX)%.lds: $(LINKFILE) @echo Creating $@ - $(SILENT)$(CROSS)$(CC) $(INC_$*) $(CFLAGS) $(OPT_$*) -E -x c - < $< | sed '/#/d' > $@ + $(SILENT)$(CROSS)$(CC) $(INC_$*) $(CFLAGS) $(OPT_$*) -E -x c - < $< \ + | sed -e '/#/d' -e '/^[[:space:]]*\.\(macro\|endm\|syntax\)/d' \ + -e '/\\/d' > $@ $(BUILDDIR)$(PFX)%.arm-elf: $(BUILDDIR)$(PFX)%.lds $(SOURCES) @echo CC -T $(notdir $^ $(SRC_$*)) diff --git a/utils/mks5lboot/dualboot/dualboot.c b/utils/mks5lboot/dualboot/dualboot.c index d58c2299e2..64b013cf16 100644 --- a/utils/mks5lboot/dualboot/dualboot.c +++ b/utils/mks5lboot/dualboot/dualboot.c @@ -105,7 +105,27 @@ static uint16_t fatal[] = { 3000,500,500, 3000,500,500, 3000,500,0, 0 }; #define sad2 (&fatal[3]) #define sad (&fatal[6]) -/* iPod Classic: decrypted hashes for known OFs */ +/* Decrypted hashes for known OFs. + * + * identify_fw() decrypts the IM3 header's data_sign with the hardware UKEY + * and looks the result up here. Anything not listed is assumed to be a RB + * bootloader (FW_RB), so a model whose OF is missing from this table cannot + * be installed to or uninstalled from - both paths bail out before writing, + * which is safe but does nothing. The table is therefore per target. */ +#ifdef IPOD_NANO3G +/* iPod Nano 3G. The decrypted data_sign is the first SIGN_SZ bytes of the + * SHA-1 of the plaintext bootloader. Each firmware's updater (aupd) carries + * that bootloader, 0x1f800 bytes, in a NOR image; these were computed from + * the aupd of every release. v1.1.3 was also read from a 4GB unit's NOR. */ +static unsigned char of_sha[][SIGN_SZ] = { + "\xC1\x3E\xBC\x68\x7C\xCD\xE8\x22\xAD\xBD\x88\x94\xA9\x95\x21\x4B", /* v1.0.1 */ + "\xC4\x57\xFD\x04\x8E\xFB\xF1\x1A\x9D\xFA\x80\x11\x1E\x3F\xA6\x3E", /* v1.0.2 */ + "\x2E\x7F\xA3\x4A\xF1\xAA\x75\x4B\x34\xF7\xB9\xC4\x58\xCF\xBD\xDB", /* v1.0.3 */ + "\x15\xA6\x6A\xBF\xA4\x3D\xFA\x76\x9F\xCE\xCE\x39\x1F\x28\xEB\x47", /* v1.1 */ + "\x60\xAC\x5A\x12\x38\x65\x0D\x2B\xC6\x63\x15\x02\xA0\x44\x84\x39" /* v1.1.2 and v1.1.3 */ +}; +#else +/* iPod Classic */ static unsigned char of_sha[][SIGN_SZ] = { "\x31\xD1\x7A\x3A\x4B\x05\xE4\x09\x6B\x58\x31\x0A\xB7\x6C\x2F\x88", /* v1.0.1 */ "\x7C\xEF\x11\xD1\x3A\x96\xBA\x1C\x1C\x73\x42\x91\xF3\xFC\xBF\x20", /* v1.0.3 */ @@ -117,6 +137,7 @@ static unsigned char of_sha[][SIGN_SZ] = { "\x06\x85\xDF\x28\xE4\xD7\xF4\x82\xC0\x73\xB0\x53\x26\xFC\xB0\xFE", /* v2.0.4 */ "\x60\x80\x7D\x33\xA8\xDE\xF8\x49\xBB\xBE\x01\x45\xFF\x62\x40\x19" /* v2.0.5 */ }; +#endif #define N_OF (int)(sizeof(of_sha)/SIGN_SZ) /* we can assume that unknown FW is a RB bootloader */ diff --git a/utils/mks5lboot/main.c b/utils/mks5lboot/main.c index 206dcf4838..ffdd395256 100644 --- a/utils/mks5lboot/main.c +++ b/utils/mks5lboot/main.c @@ -138,7 +138,8 @@ static void usage(void) " is the name of the platform (type of device) for\n" " which the DFU uninstaller will be built. Currently supported\n" " platform names are:\n" - " ipod6g: iPod Classic 6G\n" + " ipod6g: iPod Classic 6G\n" + " ipodnano3g: iPod Nano 3G\n" "\n" "Options:\n" " -p, --pid Use a specific (Product Id) USB device,\n" diff --git a/utils/mks5lboot/mkdfu.c b/utils/mks5lboot/mkdfu.c index ff9ca7041c..63336ea520 100644 --- a/utils/mks5lboot/mkdfu.c +++ b/utils/mks5lboot/mkdfu.c @@ -55,6 +55,10 @@ const struct ipod_models ipod_identity[] = "Classic 6G", "ipod6g", "ip6g", 71, dualboot_install_ipod6g, sizeof(dualboot_install_ipod6g), dualboot_uninstall_ipod6g, sizeof(dualboot_uninstall_ipod6g) }, + [MODEL_IPODNANO3G] = { + "Nano 3G", "ipodnano3g", "nn3g", 117, + dualboot_install_ipodnano3g, sizeof(dualboot_install_ipodnano3g), + dualboot_uninstall_ipodnano3g, sizeof(dualboot_uninstall_ipodnano3g) }, }; struct Im3Info s5l8702hdr = diff --git a/utils/mks5lboot/mks5lboot.h b/utils/mks5lboot/mks5lboot.h index d0185eefa9..0f41f50ecd 100644 --- a/utils/mks5lboot/mks5lboot.h +++ b/utils/mks5lboot/mks5lboot.h @@ -78,6 +78,7 @@ struct Im3Hdr enum { MODEL_UNKNOWN = -1, MODEL_IPOD6G = 0, + MODEL_IPODNANO3G, /* new models go here */ NUM_MODELS From b58c7505a0750c34d71313582dd48a348cc2e842 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Tue, 15 Sep 2026 20:41:47 +1000 Subject: [PATCH 172/408] utils: iPod Nano 3G NAND check for contributors Apple fitted at least five makers' NAND to the Nano 3G, in eighteen chip and chip-enable combinations. Rockbox's Nano 3G NAND support can only be enabled for writing on a chip that has been validated on hardware, and so far one has. This gives owners of the others a way to send what validation needs without installing anything. nano3g-check.dfu runs from DFU mode (mks5lboot --dfusend). It reads the chip ids on every chip enable, matches Apple's chip table, tries a read-only mount, and shows a short summary. It then serves the raw NAND over USB as a write-protected disk: sector 0 a text report, then 16-byte spare records for every page, then page data. Nothing is written to the iPod's NAND or NOR. nandcheck.py collect finds that disk on Linux, macOS or Windows and writes a few-MB archive: the report, every page's spare metadata and read result, and every page that is not user data or erased (the FTL and VFL control structures and Apple's bad-block records). It includes no file contents and not the serial number. The README lists the chips, how to tell which one an iPod has, and the procedure. The image is the Nano 3G bootloader built with -DNAND_CHECK from the Nano 3G NAND driver work, which is not merged yet. Tested on a 4GB Nano 3G (Hynix A514D3AD x4): sent with mks5lboot --dfusend, the disk appears within seconds, and collect reads it in 7 minutes with no ECC failures or timeouts. The archive mounts in the driver's host FTL tests with every sector resolving to its newest copy. AI provenance: developed with Claude Opus 5 (Anthropic), used through Claude Code. The model wrote most of the code and this message under Andrew Rice's direction. Any hardware testing described above was carried out by Andrew Rice, who is responsible for this change. Change-Id: Id790997768ee79451b7adeb5e9764dc8054faf4c --- utils/ipodnano3g/nandcheck/README.md | 133 ++++++++ utils/ipodnano3g/nandcheck/nandcheck.py | 352 ++++++++++++++++++++ utils/ipodnano3g/nandcheck/nano3g-check.dfu | Bin 0 -> 88140 bytes 3 files changed, 485 insertions(+) create mode 100644 utils/ipodnano3g/nandcheck/README.md create mode 100644 utils/ipodnano3g/nandcheck/nandcheck.py create mode 100644 utils/ipodnano3g/nandcheck/nano3g-check.dfu diff --git a/utils/ipodnano3g/nandcheck/README.md b/utils/ipodnano3g/nandcheck/README.md new file mode 100644 index 0000000000..bb6024b584 --- /dev/null +++ b/utils/ipodnano3g/nandcheck/README.md @@ -0,0 +1,133 @@ +# Nano 3G NAND check + +Apple fitted many different NAND chips to the iPod Nano 3G. Rockbox's Nano +3G NAND support is validated chip by chip: it writes only to chips that have +been tested on hardware, and keeps the storage read-only on the rest. This +check collects what we need to validate your chip. It **changes nothing on your +iPod**: nothing is installed, and nothing is written to its NAND. The image +runs from RAM and is gone when the iPod restarts. + +## Which chip is in your iPod + +Every Nano 3G is model A1236, whichever NAND it has, so the chip has to be +read from the iPod itself: + +1. **Capacity** narrows it down: Settings > About in Apple's firmware + shows 4GB or 8GB, and each row below is one capacity. +2. **The check's screen** names it exactly, within seconds and without + changing anything: run steps 1 and 2 below and read the `id` line (the + chip, as in the table) and the `banks` line (how many chip enables + answered). The `verdict` line says whether Rockbox knows that + combination and whether it is validated. + +If your chip is already validated, there is nothing to collect: restart +the iPod. Otherwise please carry on with steps 3 and 4. + +These are all the chips Apple's firmware (1.1.3) supports. Apple picks the +row by the id and the number of chip enables, so one id can appear twice. +Rockbox's NAND driver knows every row, but apart from the validated chip +it has so far been tested only against simulated NAND on a computer, which +is why the archives matter. + +| id | maker | capacity | chip enables | page | status | +|---|---|---|---|---|---| +| `A514D3AD` | Hynix | 4GB | 4 | 2KiB | **validated** | +| `A555D5AD` | Hynix | 8GB | 4 | 2KiB | reported, needs a check | +| `B614D5AD` | Hynix | 8GB | 4 | 4KiB | needed | +| `B614D5EC` | Samsung | 4GB | 2 | 4KiB | reported, needs a check | +| `B614D5EC` | Samsung | 8GB | 4 | 4KiB | needed | +| `2555D5EC` | Samsung | 8GB | 4 | 2KiB | needed | +| `A585D598` | Toshiba | 4GB | 2 | 2KiB | needed | +| `A585D598` | Toshiba | 8GB | 4 | 2KiB | needed | +| `BA94D598` | Toshiba | 4GB | 2 | 4KiB | needed | +| `BA94D598` | Toshiba | 8GB | 4 | 4KiB | needed | +| `A5D5D589` | Intel | 4GB | 2 | 2KiB | reported, needs a check | +| `A5D5D589` | Intel | 8GB | 4 | 2KiB | reported, needs a check | +| `3E94D589` | Intel | 4GB | 2 | 4KiB | reported, needs a check | +| `3ED5D789` | Intel | 8GB | 2 | 4KiB | needed | +| `A5D5D52C` | Micron | 4GB | 2 | 2KiB | needed | +| `A5D5D52C` | Micron | 8GB | 4 | 2KiB | needed | +| `3E94D52C` | Micron | 4GB | 2 | 4KiB | needed | +| `3ED5D72C` | Micron | 8GB | 2 | 4KiB | needed | + +"Reported" means someone has seen that chip in a unit, but no check archive +has been collected from it yet. The id is the first four READ ID bytes read +as one little-endian number: `A514D3AD` is the bytes `AD D3 14 A5`, and the +last byte is the maker (`AD` Hynix, `EC` Samsung, `98` Toshiba, `89` Intel, +`2C` Micron). When a chip is validated, the driver's chip table and this +table are updated together. + +## What you need + +- A computer (Linux, macOS or Windows) and a USB cable. +- `mks5lboot`, to send the image to the iPod: build it with + `make -C utils/mks5lboot` (libusb-1.0 development files on Linux). On + Windows, see `utils/mks5lboot/README` for the USB driver it needs. +- Python 3. + +## 1. Get the image + +`nano3g-check.dfu` in this directory is ready to use. It is the Nano 3G +bootloader built with `-DNAND_CHECK` from the Nano 3G NAND driver work, +which is not in Rockbox yet; its build shows on the iPod's screen as +"Version:" and in the report as `version`. + +## 2. Run it + +1. Put the iPod in DFU mode +2. Send the image: + ``` + utils/mks5lboot/mks5lboot --dfusend utils/ipodnano3g/nandcheck/nano3g-check.dfu + ``` +3. Within a few seconds the iPod shows "Nano 3G NAND check" and a short + summary: `battery`, `id` (the chip), `banks`, `row`, `mode`, `pagesize`, + `validated`, `ftl`, `verdict`, `model` and `swvr`. Then "Bootloader USB + mode" once the computer has picked it up. **Photograph the screen.** + +## 3. Collect + +1. Leave USB connected until `collect` finishes: unplugging ends the + iPod's USB session, and the check has to be run again. The iPod appears + as a write-protected disk named "Nano 3G NAND" (4.3 GB on a 4GB unit). + It has no file system: if Windows offers to format it, or macOS says + the disk is not readable, choose Cancel or Ignore. +2. Run, with read access to the disk: + ``` + sudo python3 utils/ipodnano3g/nandcheck/nandcheck.py collect + ``` + It finds the iPod by the report in its first sector. On Windows, run + `python utils\ipodnano3g\nandcheck\nandcheck.py collect` from an + administrator prompt. To name the disk instead, add `/dev/sdX` (Linux), + `/dev/rdiskN` (macOS) or `\\.\PhysicalDriveN` (Windows). It takes + about 7 minutes on a 4GB unit (longer on 8GB) and writes + `nandcheck--x.tar.gz`, a few MB. +3. Unplug USB, then hold MENU+SELECT to restart. The iPod comes back as it + was. + +## 4. Send + +The `.tar.gz` and the photo. The archive holds your chip's structure +records and the unit's model and firmware version - no music, file names or file contents, and not the serial number. +If the verdict says the chip is not in Apple's table, the photo is enough. + +## If something goes wrong + +- **No disk appears.** Check the screen: the summary should be there and + the last line should read "Bootloader USB mode". On Linux, `dmesg` shows + what the computer saw. Send the photo and that output. +- **"no disk carrying the Nano 3G NAND check was found"** with "no + permission to read ...": run as root or administrator. On Linux you can + instead give yourself read access to that disk until it is unplugged: + `sudo setfacl -m u:$USER:r /dev/sdX`. +- **Unplugged too early.** Restart the iPod (MENU+SELECT) and start again + from step 2. + +## What is collected + +| file | contents | +|---|---| +| `report.txt` | chip ids (all eight id bytes, every chip enable), chip table row and derived geometry, read-only mount result, verdict, model, hardware and firmware version, battery voltage, backlight register | +| `meta.bin` | every page's 12 spare bytes and read result, bank by bank (the host tests' dump format) | +| `results.bin` | every page's full read result: ECC class, most bits corrected in a chunk, erased flag | +| `pages.bin` | every page that is not user data or erased: FTL and VFL control structures, Apple's bad-block records | +| `summary.txt` | page counts by type, ECC failures, timeouts, erased-flag agreement, corrected-bit histogram | \ No newline at end of file diff --git a/utils/ipodnano3g/nandcheck/nandcheck.py b/utils/ipodnano3g/nandcheck/nandcheck.py new file mode 100644 index 0000000000..bed873131e --- /dev/null +++ b/utils/ipodnano3g/nandcheck/nandcheck.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +r"""Collect what is needed to validate an iPod Nano 3G's NAND for Rockbox. + +1. Put the iPod in DFU mode and run the check image: + mks5lboot --dfusend nano3g-check.dfu + The screen shows the chip, its table row and how a read-only mount went. + Nothing is written to the iPod. +2. Connect USB. The iPod appears as a disk of 2048-byte sectors. +3. Run, with read access to the disk (sudo on Linux/macOS, an + administrator prompt on Windows): + nandcheck.py collect + which finds the disk itself, or name it: /dev/sdX (Linux), /dev/rdiskN + (macOS), \\.\PhysicalDriveN (Windows). + This writes nandcheck--x.tar.gz: the report, the spare + metadata of every page and every page that is not file data - the VFL + and FTL control structures and Apple's bad-block records. No file + contents are included. It takes about 7 minutes on a 4GB unit. + Add --full to include every page's data as well (4-8 GB, and it contains + whatever is stored on the iPod). + +The check image serves: + sector 0 text report + 1 + block * banks + bank 128 records of 16 bytes: 3 spare words + and a result word, little endian: -1 on + a timeout, else the ECC class (0 clean, + 1 corrected, 3 failed) | most corrected + bits in a chunk << 8 | 0x10000 erased + metaend + page * secperpage page data, bank by bank + +The archive's meta.bin keeps the dump convention the host tests use (result +0 erased, 1 written, 3 failed, -1 timed out); results.bin has every page's +full result word. + + nandcheck.py collect [DISK|auto] [--out DIR] [--full] + nandcheck.py mkimage DUMPDIR IMAGE (testing: a sparse check-image + view of a 4GB unit's dump) +""" +import argparse +import hashlib +import io +import mmap +import os +import stat +import struct +import sys +import tarfile +import time + +SECTOR = 2048 +RECORD = 16 +MAGIC = "nano3g-nandcheck 1" +DATA_TYPES = (0x40, 0x41) +ERASED = 0xff + + +def is_control(kind): + """Every page but user data and erased ones: FTL control (0x43-0x4f), + VFL contexts (0x80) and whatever else Apple keeps, such as its bad-block + records""" + return kind not in DATA_TYPES and kind != ERASED + + +WINDOWS = sys.platform == "win32" + + +class Disk: + """A disk opened read-only, read in whole sectors""" + + def __init__(self, path): + flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) + direct = False + if not WINDOWS: + try: + if stat.S_ISBLK(os.stat(path).st_mode) and \ + hasattr(os, "O_DIRECT"): + flags |= os.O_DIRECT # no readahead of unwanted sectors + direct = True + except OSError: + pass + self.fd = os.open(path, flags) + self.direct = direct + self.buf = mmap.mmap(-1, SECTOR * 256) # page-aligned for O_DIRECT + + def read(self, sector, count): + out = bytearray() + while count: + n = min(count, 256) + if self.direct: + view = memoryview(self.buf)[:n * SECTOR] + got = os.preadv(self.fd, [view], sector * SECTOR) + data = view[:got] + else: + # Windows raw disks and macOS: seek and read whole sectors + os.lseek(self.fd, sector * SECTOR, os.SEEK_SET) + data = os.read(self.fd, n * SECTOR) + got = len(data) + if got != n * SECTOR: + raise IOError("short read at sector %d" % sector) + out += data + sector += n + count -= n + return bytes(out) + + def close(self): + os.close(self.fd) + + +def candidate_disks(): + """Whole disks this system might show the iPod as""" + if WINDOWS: + return [r"\\.\PhysicalDrive%d" % i for i in range(32)] + if sys.platform == "darwin": + return ["/dev/rdisk%d" % i for i in range(32)] + return sorted("/dev/" + d for d in os.listdir("/sys/block") + if d.startswith(("sd", "mmcblk", "vd"))) + + +def find_disk(): + """The disk whose sector 0 is the check image's report""" + denied = [] + for path in candidate_disks(): + try: + disk = Disk(path) + except PermissionError: + denied.append(path) + continue + except OSError: + continue + try: + first = disk.read(0, 1) + except (OSError, IOError): + first = b"" + disk.close() + if first.startswith(MAGIC.encode()): + return path + hint = "" + if denied: + hint = ("\nno permission to read %s - run as administrator/root, " + "or grant read access to the disk" % ", ".join(denied)) + sys.exit("no disk carrying the Nano 3G NAND check was found" + hint) + + +def parse_report(sector0): + text = sector0.split(b"\0", 1)[0].decode("ascii", "replace") + lines = text.splitlines() + if not lines or lines[0] != MAGIC: + sys.exit("not the Nano 3G NAND check image (sector 0 is %r)" + % lines[:1]) + rep = {} + for line in lines[1:]: + key, _, value = line.partition(" ") + rep[key] = value + return text, rep + + +def ftl_meaning(rc): + """What ftl_init()'s result says about the mount""" + try: + rc = int(rc) + except ValueError: + return "not tried" + fixed = {0: "mounted", -1: "no geometry", + -2: "geometry beyond the FTL's limits", + -3: "no VFL context found", + -4: "layout or page size not supported", + -5: "VFL context does not fit the chip table row"} + if rc in fixed: + return fixed[rc] + if -20 < rc <= -10: + return "no FTL context in the control blocks" + if rc <= -20: + return "FTL context or restore inconsistent" + return "unknown" + + +def progress(done, total, start, what): + rate = done / max(time.time() - start, 1e-3) + eta = (total - done) / rate if rate else 0 + sys.stderr.write("\r%s %5.1f%% %d/%d eta %dm%02ds " % ( + what, 100.0 * done / total, done, total, eta // 60, eta % 60)) + + +def collect(args): + # before the minutes of reading, not after + os.makedirs(args.out, exist_ok=True) + path = find_disk() if args.disk == "auto" else args.disk + print("reading %s" % path) + disk = Disk(path) + text, rep = parse_report(disk.read(0, 1)) + print(text.rstrip()) + ident = rep.get("ids", "unknown").split()[0] + name = "nandcheck-%s-x%s" % (ident, rep.get("banks", "0")) + out = os.path.join(args.out, name + ".tar.gz") + files = {"report.txt": text.encode()} + + if rep.get("row", "none") == "none": + print("\nThe chip is not in Apple's table; only the report is saved.") + else: + banks, blocks = int(rep["banks"]), int(rep["blocks"]) + ppb, pagesize = int(rep["ppb"]), int(rep["pagesize"]) + spp = pagesize // SECTOR + metaend = 1 + blocks * banks + ftl = rep.get("ftl", "?") + print("\nread-only mount: %s (%s)" % (ftl, ftl_meaning(ftl))) + + # Spare records of every page, stored bank by bank like a dump + meta = bytearray(banks * blocks * ppb * RECORD) + results = bytearray(banks * blocks * ppb * 4) + start = time.time() + step = 8 * banks + for s in range(1, metaend, step): + n = min(step, metaend - s) + data = disk.read(s, n) + for i in range(n): + block, bank = divmod(s - 1 + i, banks) + off = (bank * blocks + block) * ppb * RECORD + meta[off:off + ppb * RECORD] = data[i * SECTOR: + (i + 1) * SECTOR] + progress(s - 1 + n, metaend - 1, start, "spare records") + sys.stderr.write("\n") + + # The control pages, and a summary of what the medium holds + counts = {"data": 0, "erased": 0, "ftl": 0, "vfl": 0, "other": 0, + "ecc-failed": 0, "timeout": 0, + "erased-flag-but-written": 0, "erased-type-no-flag": 0} + corrected = [0] * 9 + control = [] + for g in range(banks * blocks * ppb): + off = g * RECORD + word = struct.unpack_from("> 8) & 0xff, 8)] += 1 + norm = 0 if erased else 3 if failed else 1 + struct.pack_into("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Ƀ-Iw!jT~ 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 literal 0 HcmV?d00001 From dc37bb1ba0909640a336f944f0f646656f68134b Mon Sep 17 00:00:00 2001 From: delacor Date: Tue, 15 Sep 2026 17:16:07 +0200 Subject: [PATCH 173/408] tagtree: warn if tagnavi.config is missing instead of freezing Database still opened an empty list when the config failed to parse. This results in the Device being complete unresponsive and needs a hard-reboot. This shows a file-not-found splash instead and return to the previous menu. Change-Id: Ifbb6998c46a4685eae486cffd89833f93a172713 --- apps/root_menu.c | 7 +++++++ apps/tagtree.c | 12 ++++++++++-- apps/tagtree.h | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/root_menu.c b/apps/root_menu.c index a989475740..918669b245 100644 --- a/apps/root_menu.c +++ b/apps/root_menu.c @@ -68,6 +68,7 @@ #endif #ifdef HAVE_TAGCACHE #include "tagcache.h" +#include "tagtree.h" #endif #include "language.h" #include "plugin.h" @@ -269,6 +270,12 @@ static int browser(void* param) } if (!tagcache_is_usable()) return GO_TO_PREVIOUS; + if (!tagtree_ready()) + { + splashf(HZ*3, "%s\n%s", str(LANG_FILE_NOT_FOUND), + ROCKBOX_DIR "/tagnavi.config"); + return GO_TO_PREVIOUS; + } filter = SHOW_ID3DB; last_ft_dirlevel = tc->dirlevel; tc->dirlevel = last_db_dirlevel; diff --git a/apps/tagtree.c b/apps/tagtree.c index 83aa2fe21e..89889dfd8d 100644 --- a/apps/tagtree.c +++ b/apps/tagtree.c @@ -1467,9 +1467,16 @@ static bool initialize_tagtree(void) /* also used when user selects 'Reload' in return true; } +static bool tagnavi_loaded; + void tagtree_init(void) { - initialize_tagtree(); + tagnavi_loaded = initialize_tagtree(); +} + +bool tagtree_ready(void) +{ + return tagnavi_loaded; } static int format_str(struct tagcache_search *tcs, struct display_format *fmt, @@ -2097,7 +2104,8 @@ int tagtree_load(struct tree_context* c) { splash(HZ, ID2P(LANG_WAIT)); tagtree_unload(c); - if (!initialize_tagtree()) + tagnavi_loaded = initialize_tagtree(); + if (!tagnavi_loaded) return 0; } c->dirlevel = 0; diff --git a/apps/tagtree.h b/apps/tagtree.h index 8b24dd162a..b96b4fdf82 100644 --- a/apps/tagtree.h +++ b/apps/tagtree.h @@ -33,6 +33,7 @@ int tagtree_export(void); int tagtree_import(void); void tagtree_init(void) INIT_ATTR; +bool tagtree_ready(void); int tagtree_enter(struct tree_context* c, bool is_visible); void tagtree_exit(struct tree_context* c, bool is_visible); int tagtree_load(struct tree_context* c); From 2adcfa08cf85dd8804b56efc94f8da9cc26ca2b9 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Sat, 12 Sep 2026 22:41:01 +1000 Subject: [PATCH 174/408] s5l8702: add the Nano 3G's flash controller registers The S5L8702 has two flash memory controllers; the BootROM uses the first at 0x38a00000 and addresses the second at +0x400. FMC_BASE was defined for the S5L8700 and S5L8701 only. Also add the registers Apple's NANDReadPage (BootROM 0x20009910) uses that are not in the existing S5L8700 set, and three FMCSTAT bits. Their real names are unknown, so they are named for what they were observed to do and the comments say which are inferred rather than measured. No functional change: this header only adds definitions. AI provenance: developed with Claude Opus 5 (Anthropic), used through Claude Code. The model wrote most of the code and this message under Andrew Rice's direction. Any hardware testing described above was carried out by Andrew Rice, who is responsible for this change. Change-Id: Ib8c808128abc9a117cb811fe1fe47bffa4ec0abe --- firmware/export/s5l87xx.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/firmware/export/s5l87xx.h b/firmware/export/s5l87xx.h index 0ece79c0a6..2dcef72960 100644 --- a/firmware/export/s5l87xx.h +++ b/firmware/export/s5l87xx.h @@ -870,6 +870,9 @@ #define FMC_BASE 0x3C200000 #elif CONFIG_CPU==S5L8701 #define FMC_BASE 0x39400000 +#elif CONFIG_CPU==S5L8702 +/* The first of two controllers; the BootROM addresses the second at +0x400 */ +#define FMC_BASE 0x38a00000 #endif #define FMCTRL0 (*(REG32_PTR_T)(FMC_BASE + 0x0000)) /* Control Register0 */ @@ -925,8 +928,19 @@ #define RSSYND3_1 (*(REG32_PTR_T)(FMC_BASE + 0x0184)) /* On-the-fly Synd Register3[63:32] */ #define RSSYND3_2 (*(REG32_PTR_T)(FMC_BASE + 0x0188)) /* On-the-fly Synd Register3[71:64] */ #define FLAGSYND (*(REG32_PTR_T)(FMC_BASE + 0x0190)) /* On-the-fly ECC Result Flag */ + +/* S5L8702 registers used by Apple's NANDReadPage (BootROM 0x20009910). + * The real names are unknown. */ +#define FMUNK78 (*(REG32_PTR_T)(FMC_BASE + 0x0078)) /* spare decode setup, 0x5140 */ +#define FMUNK7C (*(REG32_PTR_T)(FMC_BASE + 0x007C)) /* bit 1 start/busy; FMSYND5..7 */ +#define FMTRANS0 (*(REG32_PTR_T)(FMC_BASE + 0x080C)) /* per-chunk ECC correction kick */ +#define FMUNK810 (*(REG32_PTR_T)(FMC_BASE + 0x0810)) /* bit 0: correction failed, inferred */ +#define FMTRANS1 (*(REG32_PTR_T)(FMC_BASE + 0x0814)) /* correction mode, always 0x01000180 */ +#define FMTRANSSTAT (*(REG32_PTR_T)(FMC_BASE + 0x0840)) /* correction status, w1c; bit 2 done */ + #define FMCTRL0_ENABLEDMA (1 << 10) #define FMCTRL0_UNK1 (1 << 11) +#define FMCTRL0_AUTOXFER (1 << 24) /* S5L8702: write the chunk to the address in FMDATAW0 */ #define FMCTRL1_DOTRANSADDR (1 << 0) #define FMCTRL1_DOREADDATA (1 << 1) #define FMCTRL1_DOWRITEDATA (1 << 2) @@ -940,6 +954,9 @@ #define FMCSTAT_BANK1READY (1 << 5) #define FMCSTAT_BANK2READY (1 << 6) #define FMCSTAT_BANK3READY (1 << 7) +#define FMCSTAT_UNK20 (1 << 20) /* S5L8702: chunk written to memory */ +#define FMCSTAT_STATUSREADY (1 << 23) /* S5L8702: set after READ STATUS; not a ready bit */ +#define FMCSTAT_UNK27 (1 << 27) /* S5L8702: chunk has ECC errors (inferred) */ /* 13. SECURE DIGITAL CARD INTERFACE (SDCI) */ #if CONFIG_CPU==S5L8700 || CONFIG_CPU==S5L8701 From 66bc0728d5e422ff26496140f168b8a49540be08 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Sat, 12 Sep 2026 22:41:36 +1000 Subject: [PATCH 175/408] ipodnano3g: preserve PMU register 0x10 bit 2, which the NAND needs pmu_preinit() masked register 0x10 with 0xdb, which clears bits 2 and 5. With bit 2 clear the NAND chip does not answer and nand_init() fails. Masking with 0xdf clears only bit 5 and leaves bit 2 alone. Measured on a 4GB Nano 3G. Every build that had worked carried debug PMU reads, i.e. extra I2C delay, so delay was the confound to rule out and each mask was tried with and without it: 0xdb, no extra delay dead 0xdb, extra delay dead 0xdf, extra delay reads (3 runs) 0xdf, no extra delay reads One early 0xdf build without instrumentation failed once (rc=-1). That has not been explained; the final validation run is the same configuration and reads correctly. Reading the register back after pmu_preinit() gives 0xdf. The new mask keeps bit 2 and "| 0x8" only sets bit 3, so bit 2 is set as the BootROM left it - the old mask was clearing it. Bit 5 is hidden by the mask, so what the ROM leaves there is not known. What bit 2 does is not established. The old comment guessed "bit4 is related to NAND, LDO_0x15 on/off"; the register does sit among the NAND supply settings (0x15 reads back the value commented as Vnand), but no datasheet was consulted, so the new comment states only the observed effect. AI provenance: developed with Claude Opus 5 (Anthropic), used through Claude Code. The model wrote most of the code and this message under Andrew Rice's direction. Any hardware testing described above was carried out by Andrew Rice, who is responsible for this change. Change-Id: Iee73f876b7b6a5c85e09f8bf1c5ac174a49409e0 --- firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c index c7cfebf7fc..08f0ef6ff9 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c @@ -339,7 +339,7 @@ void pmu_preinit(void) pmu_wr(0x16, 0x14); pmu_wr(0x15, 0x14); // TBC: Vnand = 2000 + val*50 = 3000 mV pmu_wr(0x18, 0x18); // TBC TBC TBC: Vaccy = 3200 mV ??? - pmu_wr(0x10, (pmu_rd(0x10) & 0xdb) | 0x8); // TBC: bit4 is related to NAND, LDO_0x15 on/off ??? + pmu_wr(0x10, (pmu_rd(0x10) & 0xdf) | 0x8); /* keep bit 2: NAND needs it */ // TBC: 0x30, 0x31 y 0x32 seems related to ADC (norboot) pmu_wr(0x34, 0x72); // TBC: en DA9030: TBATHIGH (0-255, TBAT high temperature threshold From dd164cadb158a50f71b263eebcb005f950ea99f5 Mon Sep 17 00:00:00 2001 From: Michael McAllister Date: Tue, 8 Sep 2026 23:29:23 +0100 Subject: [PATCH 176/408] jztool: support X1600 USB recovery boot Add the HiBy R1 and X1600 USB boot path, sharing the boot-package reader with X1000. Load the returning USB stage separately from the flash SPL, check its DDR result, then upload and run the second-stage bootloader. Built on macOS; error paths checked with mocked USB transfers. Change-Id: I501c1ffdb9e3d5f0c76a379ec6d229ec579bf100 --- utils/jztool/Makefile | 5 +- utils/jztool/include/jztool.h | 3 + utils/jztool/jztool.c | 49 ++++++++- utils/jztool/src/boot_file.c | 88 +++++++++++++++ utils/jztool/src/device_info.c | 31 ++++++ utils/jztool/src/jztool_private.h | 14 +++ utils/jztool/src/usb.c | 29 ++++- utils/jztool/src/x1000.c | 74 +------------ utils/jztool/src/x1600.c | 172 ++++++++++++++++++++++++++++++ 9 files changed, 389 insertions(+), 76 deletions(-) create mode 100644 utils/jztool/src/boot_file.c create mode 100644 utils/jztool/src/x1600.c diff --git a/utils/jztool/Makefile b/utils/jztool/Makefile index 02436d47bc..a8483d1f0e 100644 --- a/utils/jztool/Makefile +++ b/utils/jztool/Makefile @@ -14,8 +14,9 @@ else CFLAGS += -O0 -ggdb endif -LIBSOURCES := src/buffer.c src/context.c src/device_info.c \ - src/identify_file.c src/ucl_unpack.c src/usb.c src/x1000.c +LIBSOURCES := src/boot_file.c src/buffer.c src/context.c src/device_info.c \ + src/identify_file.c src/ucl_unpack.c src/usb.c src/x1000.c \ + src/x1600.c SOURCES := $(LIBSOURCES) jztool.c EXTRADEPS := libucl.a libmicrotar.a diff --git a/utils/jztool/include/jztool.h b/utils/jztool/include/jztool.h index 732e65a0ee..b1b2c0f8b3 100644 --- a/utils/jztool/include/jztool.h +++ b/utils/jztool/include/jztool.h @@ -82,11 +82,13 @@ enum jz_device_type { JZ_DEVICE_SHANLINGQ1, JZ_DEVICE_EROSQ, JZ_DEVICE_SHANLINGM0PRO, + JZ_DEVICE_HIBYR1, JZ_NUM_DEVICES, }; enum jz_cpu_type { JZ_CPU_X1000, + JZ_CPU_X1600, JZ_NUM_CPUS, }; @@ -178,6 +180,7 @@ int jz_usb_get_cpu_info(jz_usbdev* dev, char* buffer, size_t buflen); */ int jz_x1000_boot(jz_usbdev* dev, jz_device_type type, const char* filename); +int jz_x1600_boot(jz_usbdev* dev, jz_device_type type, const char* filename); /****************************************************************************** * Buffer API and other functions diff --git a/utils/jztool/jztool.c b/utils/jztool/jztool.c index e5fbad6b40..8ac6efbaef 100644 --- a/utils/jztool/jztool.c +++ b/utils/jztool/jztool.c @@ -70,10 +70,13 @@ void usage_x1000(void) exit(4); } -int cmdline_x1000(int argc, char** argv) +/* The "load" command. Identical for every Ingenic CPU except for the usage + * text and which jz_*_boot() does the work. */ +int cmdline_load(int argc, char** argv, void (*usage_fn)(void), + int (*boot_fn)(jz_usbdev*, jz_device_type, const char*)) { if(argc < 2 || strcmp(argv[0], "load")) { - usage_x1000(); + usage_fn(); return 2; } @@ -83,7 +86,7 @@ int cmdline_x1000(int argc, char** argv) return 1; } - rc = jz_x1000_boot(usbdev, dev_info->device_type, argv[1]); + rc = boot_fn(usbdev, dev_info->device_type, argv[1]); if(rc < 0) { jz_log(jz, JZ_LOG_ERROR, "Boot failed: %d", rc); return 1; @@ -92,6 +95,41 @@ int cmdline_x1000(int argc, char** argv) return 0; } +void usage_x1600(void) +{ + printf( +"Usage:\n" +" jztool hibyr1 load \n" +"\n" +"The 'load' command boots the Rockbox bootloader in recovery mode over the\n" +"X1600 BootROM's USB protocol. The player must be in USB boot mode.\n" +"\n" +"To put a HiBy R1 in USB boot mode:\n" +"\n" +"1. Hold POWER until the player is fully off.\n" +"2. Unplug the USB cable.\n" +"3. Hold down NEXT (the lower button on the right-hand side).\n" +"4. While still holding NEXT, plug the USB cable in. Do NOT press POWER --\n" +" inserting the cable is what powers the SoC, and boot_sel is sampled at\n" +" that instant, so NEXT has to be held before the cable goes in.\n" +"5. Release NEXT after about three seconds.\n" +"\n" +"The player gives no visible indication. It should appear as USB device\n" +"a108:eaef, \"Ingenic USB BOOT DEVICE\". Prefer a port directly on the\n" +"computer over a hub chain.\n" +"\n" +"If the BootROM stops responding, a full power-off is required -- unplug the\n" +"cable and let the player discharge, then start again from step 1. Replugging\n" +"alone is not enough.\n" +"\n" +"Note for Windows users: you need to install the WinUSB driver using a\n" +"3rd-party tool such as Zadig before this tool\n" +"can access your player in USB boot mode.\n" +"\n"); + + exit(4); +} + void usage(void) { printf("Usage:\n" @@ -203,7 +241,10 @@ int main(int argc, char** argv) case JZ_DEVICE_SHANLINGQ1: case JZ_DEVICE_EROSQ: case JZ_DEVICE_SHANLINGM0PRO: - return cmdline_x1000(argc, argv); + return cmdline_load(argc, argv, usage_x1000, jz_x1000_boot); + + case JZ_DEVICE_HIBYR1: + return cmdline_load(argc, argv, usage_x1600, jz_x1600_boot); default: jz_log(jz, JZ_LOG_ERROR, "INTERNAL ERROR: unhandled device type"); diff --git a/utils/jztool/src/boot_file.c b/utils/jztool/src/boot_file.c new file mode 100644 index 0000000000..4a0273e5fb --- /dev/null +++ b/utils/jztool/src/boot_file.c @@ -0,0 +1,88 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2021 Aidan MacDonald + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* Reading members out of a Rockbox boot package. The package layout is the + * same whatever the SoC is, so the per-CPU boot code shares this. */ + +#include "jztool.h" +#include "jztool_private.h" +#include "microtar-stdio.h" +#include + +int jz_boot_get_file(jz_context* jz, struct mtar* tar, const char* file, + unsigned int flags, jz_buffer** buf) +{ + jz_buffer* buffer = NULL; + const mtar_header_t* h; + int rc; + + rc = mtar_find(tar, file); + if(rc != MTAR_ESUCCESS) { + if(!(flags & JZ_BOOT_OPTIONAL)) + jz_log(jz, JZ_LOG_ERROR, "can't find %s in boot file, tar error %d", + file, rc); + return JZ_ERR_OPEN_FILE; + } + + h = mtar_get_header(tar); + buffer = jz_buffer_alloc(h->size, NULL); + if(!buffer) + return JZ_ERR_OUT_OF_MEMORY; + + rc = mtar_read_data(tar, buffer->data, buffer->size); + if(rc < 0 || (unsigned)rc != buffer->size) { + jz_buffer_free(buffer); + jz_log(jz, JZ_LOG_ERROR, "can't read %s in boot file, tar error %d", + file, rc); + return JZ_ERR_BAD_FILE_FORMAT; + } + + if(flags & JZ_BOOT_DECOMPRESS) { + uint32_t dst_len; + jz_buffer* nbuf = jz_ucl_unpack(buffer->data, buffer->size, &dst_len); + jz_buffer_free(buffer); + if(!nbuf) { + jz_log(jz, JZ_LOG_ERROR, "error decompressing %s in boot file", file); + return JZ_ERR_BAD_FILE_FORMAT; + } + + /* for simplicity just forget original size of buffer */ + nbuf->size = dst_len; + buffer = nbuf; + } + + *buf = buffer; + return JZ_SUCCESS; +} + +int jz_boot_show_version(jz_context* jz, jz_buffer* info_file) +{ + /* Extract the version string and log it for informational purposes */ + char* boot_version = (char*)info_file->data; + char* endpos = memchr(boot_version, '\n', info_file->size); + if(!endpos) { + jz_log(jz, JZ_LOG_ERROR, "invalid metadata in boot file"); + return JZ_ERR_BAD_FILE_FORMAT; + } + + *endpos = 0; + jz_log(jz, JZ_LOG_NOTICE, "Rockbox bootloader version: %s", boot_version); + return JZ_SUCCESS; +} diff --git a/utils/jztool/src/device_info.c b/utils/jztool/src/device_info.c index 69cce405cf..77baa4c303 100644 --- a/utils/jztool/src/device_info.c +++ b/utils/jztool/src/device_info.c @@ -58,6 +58,19 @@ static const jz_device_info infotable[JZ_NUM_DEVICES] = { .vendor_id = 0x0525, .product_id = 0xa4a5, }, + [JZ_DEVICE_HIBYR1] = { + .name = "hibyr1", + .file_ext = "r1", + .description = "HiBy R1", + .device_type = JZ_DEVICE_HIBYR1, + .cpu_type = JZ_CPU_X1600, + /* NOTE(x1600): these are the mass-storage-mode IDs and are UNVERIFIED. + * The only IDs confirmed on the device are the BootROM's, which live in + * the cputable entry below. Fill these in from `lsusb` with the player + * booted into its normal firmware and mounted as UMS. */ + .vendor_id = 0x0000, + .product_id = 0x0000, + }, }; static const jz_cpu_info cputable[JZ_NUM_CPUS] = { @@ -70,6 +83,24 @@ static const jz_cpu_info cputable[JZ_NUM_CPUS] = { .stage2_load_addr = 0x80004000, .stage2_exec_addr = 0x80004000, }, + [JZ_CPU_X1600] = { + /* A HiBy R1 in BootROM USB mode enumerates as + * a108:eaef "Ingenic USB BOOT DEVICE", interface class 255, bulk + * endpoints 0x01 OUT / 0x81 IN with 512-byte packets, and answers + * GET_CPU_INFO (with bmRequestType 0xC0, see src/usb.c) with "X1600". */ + .info_str = "X1600", + .vendor_id = 0xa108, + .product_id = 0xeaef, + + /* Raw USB stage1, linked above the BootROM's cache area (PM 34.3). + * Unlike the X1000 flash SPL, this image has no header to skip. */ + .stage1_load_addr = 0x8000a000, + .stage1_exec_addr = 0x8000a000, + + /* Matches X1600_BOOT_LOAD_ADDR in firmware/export/x1600.h. */ + .stage2_load_addr = 0x80100000, + .stage2_exec_addr = 0x80100000, + }, }; /** \brief Lookup info for a device by type, returns NULL if not found. */ diff --git a/utils/jztool/src/jztool_private.h b/utils/jztool/src/jztool_private.h index 8843dc8304..016faf4095 100644 --- a/utils/jztool/src/jztool_private.h +++ b/utils/jztool/src/jztool_private.h @@ -40,4 +40,18 @@ struct jz_usbdev { int jz_context_ref_libusb(jz_context* jz); void jz_context_unref_libusb(jz_context* jz); +/* Boot package access, shared by the per-CPU boot code. Declared as + * "struct mtar" rather than mtar_t so this header need not pull in + * microtar.h; it is the same type microtar typedefs. */ +struct mtar; + +enum { + JZ_BOOT_DECOMPRESS = 0x01, + JZ_BOOT_OPTIONAL = 0x02, +}; + +int jz_boot_get_file(jz_context* jz, struct mtar* tar, const char* file, + unsigned int flags, jz_buffer** buf); +int jz_boot_show_version(jz_context* jz, jz_buffer* info_file); + #endif /* JZTOOL_PRIVATE_H */ diff --git a/utils/jztool/src/usb.c b/utils/jztool/src/usb.c index 7747f3c27e..ac11eb4cd7 100644 --- a/utils/jztool/src/usb.c +++ b/utils/jztool/src/usb.c @@ -143,13 +143,36 @@ void jz_usb_close(jz_usbdev* dev) free(dev); } +// Direction of each vendor request's control transfer. +// +// GET_CPU_INFO returns data, so it must be a device-to-host (IN) request. The +// Ingenic documentation says otherwise -- X1600 PM Table 34-6 lists it as +// "bmRequestType = 40H (D7 0: Host to Device)" *with* wLength = 0008H, which is +// self-contradictory -- and jztool originally followed the documentation and +// hard-coded LIBUSB_ENDPOINT_OUT for every request. +// +// VERIFIED on X1600 hardware (HiBy R1 in BootROM USB mode, a108:eaef): request +// 0 with bmRequestType 0x40 fails with EIO, and with 0xC0 it returns the 5-byte +// string "X1600". Every other request carries no data and stays OUT. +static const unsigned char vendor_req_dir[6] = { + [VR_GET_CPU_INFO] = LIBUSB_ENDPOINT_IN, + [VR_SET_DATA_ADDRESS] = LIBUSB_ENDPOINT_OUT, + [VR_SET_DATA_LENGTH] = LIBUSB_ENDPOINT_OUT, + [VR_FLUSH_CACHES] = LIBUSB_ENDPOINT_OUT, + [VR_PROGRAM_START1] = LIBUSB_ENDPOINT_OUT, + [VR_PROGRAM_START2] = LIBUSB_ENDPOINT_OUT, +}; + // Does an Ingenic-specific vendor request // Written with X1000 in mind but other Ingenic CPUs have the same commands static int jz_usb_vendor_req(jz_usbdev* dev, int req, uint32_t arg, void* buffer, int buflen) { + if(req < 0 || req >= (int)(sizeof(vendor_req_dir)/sizeof(vendor_req_dir[0]))) + return JZ_ERR_OTHER; + int rc = libusb_control_transfer(dev->handle, - LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE, + vendor_req_dir[req] | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE, req, arg >> 16, arg & 0xffff, buffer, buflen, 1000); if(rc < 0) { @@ -276,7 +299,11 @@ int jz_usb_flush_caches(jz_usbdev* dev) */ int jz_usb_get_cpu_info(jz_usbdev* dev, char* buffer, size_t buflen) { + /* Must be zeroed first: the X1000 answers with 8 bytes ("X1000_v1") but the + * X1600 answers with only 5 ("X1600"), and jz_usb_vendor_req() does not + * report the transferred length, so the tail would otherwise be garbage. */ char tmpbuf[JZ_CPUINFO_BUFLEN]; + memset(tmpbuf, 0, sizeof(tmpbuf)); int rc = jz_usb_vendor_req(dev, VR_GET_CPU_INFO, 0, tmpbuf, 8); if(rc != JZ_SUCCESS) return rc; diff --git a/utils/jztool/src/x1000.c b/utils/jztool/src/x1000.c index 7c3b08b54b..368e405405 100644 --- a/utils/jztool/src/x1000.c +++ b/utils/jztool/src/x1000.c @@ -100,70 +100,6 @@ static int run_stage2(jz_usbdev* dev, jz_buffer* buf) return jz_usb_start2(dev, load_addr); } -enum { - F_DECOMPRESS = 0x01, - F_OPTIONAL = 0x02, -}; - -static int get_file(jz_context* jz, mtar_t* tar, const char* file, - unsigned int flags, jz_buffer** buf) -{ - jz_buffer* buffer = NULL; - const mtar_header_t* h; - int rc; - - rc = mtar_find(tar, file); - if(rc != MTAR_ESUCCESS) { - if(!(flags & F_OPTIONAL)) - jz_log(jz, JZ_LOG_ERROR, "can't find %s in boot file, tar error %d", file, rc); - return JZ_ERR_OPEN_FILE; - } - - h = mtar_get_header(tar); - buffer = jz_buffer_alloc(h->size, NULL); - if(!buffer) - return JZ_ERR_OUT_OF_MEMORY; - - rc = mtar_read_data(tar, buffer->data, buffer->size); - if(rc < 0 || (unsigned)rc != buffer->size) { - jz_buffer_free(buffer); - jz_log(jz, JZ_LOG_ERROR, "can't read %s in boot file, tar error %d", file, rc); - return JZ_ERR_BAD_FILE_FORMAT; - } - - if(flags & F_DECOMPRESS) { - uint32_t dst_len; - jz_buffer* nbuf = jz_ucl_unpack(buffer->data, buffer->size, &dst_len); - jz_buffer_free(buffer); - if(!nbuf) { - jz_log(jz, JZ_LOG_ERROR, "error decompressing %s in boot file", file); - return JZ_ERR_BAD_FILE_FORMAT; - } - - /* for simplicity just forget original size of buffer */ - nbuf->size = dst_len; - buffer = nbuf; - } - - *buf = buffer; - return JZ_SUCCESS; -} - -static int show_version(jz_context* jz, jz_buffer* info_file) -{ - /* Extract the version string and log it for informational purposes */ - char* boot_version = (char*)info_file->data; - char* endpos = memchr(boot_version, '\n', info_file->size); - if(!endpos) { - jz_log(jz, JZ_LOG_ERROR, "invalid metadata in boot file"); - return JZ_ERR_BAD_FILE_FORMAT; - } - - *endpos = 0; - jz_log(jz, JZ_LOG_NOTICE, "Rockbox bootloader version: %s", boot_version); - return JZ_SUCCESS; -} - /** \brief Load the Rockbox bootloader on an X1000 device * \param dev USB device freshly returned by jz_usb_open() * \param filename Path to the "bootloader.target" file @@ -193,7 +129,7 @@ int jz_x1000_boot(jz_usbdev* dev, jz_device_type type, const char* filename) } /* Extract all necessary files */ - rc = get_file(dev->jz, &tar, spl_filename, false, &spl); + rc = jz_boot_get_file(dev->jz, &tar, spl_filename, false, &spl); if(rc != JZ_SUCCESS) goto error; @@ -206,8 +142,8 @@ int jz_x1000_boot(jz_usbdev* dev, jz_device_type type, const char* filename) */ const char* bl_files[2] = {"bootloader2.ucl", "bootloader.ucl"}; for(int i = 0; i < 2; ++i) { - rc = get_file(dev->jz, &tar, bl_files[i], - F_DECOMPRESS|F_OPTIONAL, &bootloader); + rc = jz_boot_get_file(dev->jz, &tar, bl_files[i], + JZ_BOOT_DECOMPRESS|JZ_BOOT_OPTIONAL, &bootloader); if(rc == JZ_SUCCESS) break; else if(rc != JZ_ERR_OPEN_FILE) @@ -219,12 +155,12 @@ int jz_x1000_boot(jz_usbdev* dev, jz_device_type type, const char* filename) goto error; } - rc = get_file(dev->jz, &tar, "bootloader-info.txt", false, &info_file); + rc = jz_boot_get_file(dev->jz, &tar, "bootloader-info.txt", false, &info_file); if(rc != JZ_SUCCESS) goto error; /* Display the version string */ - rc = show_version(dev->jz, info_file); + rc = jz_boot_show_version(dev->jz, info_file); if(rc != JZ_SUCCESS) goto error; diff --git a/utils/jztool/src/x1600.c b/utils/jztool/src/x1600.c new file mode 100644 index 0000000000..6f82868148 --- /dev/null +++ b/utils/jztool/src/x1600.c @@ -0,0 +1,172 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 Michael McAllister + * Derived from src/x1000.c, Copyright (C) 2021 Aidan MacDonald + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* X1600 USB recovery uses a separate returning stage1 to initialise DRAM. + * The flash SPL has a different link address and is not used by this path. */ + +#include "jztool.h" +#include "jztool_private.h" +#include "microtar-stdio.h" +#include +#include + +/* Result block written by usbstage1-x1600.c before returning to BootROM. */ +#define X1600_STAGE1_RESULT_ADDR 0x8000f000 +#define X1600_STAGE1_MAGIC 0xc0def00d + +static uint32_t get_le32(const uint8_t* p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +/** \brief Load the Rockbox bootloader on an X1600 device + * \param dev USB device freshly returned by jz_usb_open() + * \param type Device type, used to pick the file extension + * \param filename Path to the "bootloader." update package + * \return either JZ_SUCCESS or an error code + */ +int jz_x1600_boot(jz_usbdev* dev, jz_device_type type, const char* filename) +{ + const jz_device_info* dev_info; + const jz_cpu_info* cpu_info; + char stage1_filename[32]; + uint8_t stage1_result[8]; + jz_buffer* stage1 = NULL, *bootloader = NULL, *info_file = NULL; + mtar_t tar; + int rc; + + dev_info = jz_get_device_info(type); + if(!dev_info) + return JZ_ERR_OTHER; + + cpu_info = jz_get_cpu_info(dev_info->cpu_type); + if(!cpu_info) + return JZ_ERR_OTHER; + + /* Load the raw USB stage1, without the flash SPL header. */ + sprintf(stage1_filename, "usbstage1.%s", dev_info->file_ext); + + rc = mtar_open(&tar, filename, "rb"); + if(rc != MTAR_ESUCCESS) { + jz_log(dev->jz, JZ_LOG_ERROR, "cannot open file %s (tar error: %d)", + filename, rc); + return JZ_ERR_OPEN_FILE; + } + + rc = jz_boot_get_file(dev->jz, &tar, stage1_filename, 0, &stage1); + if(rc != JZ_SUCCESS) { + jz_log(dev->jz, JZ_LOG_ERROR, + "This update package has no %s member, so it cannot be USB " + "booted. The X1600 needs a stage1 payload linked at 0x%08lx, " + "separate from the flash SPL.", + stage1_filename, (unsigned long)cpu_info->stage1_load_addr); + goto error; + } + + if(stage1->size > 20 * 1024) { + jz_log(dev->jz, JZ_LOG_ERROR, + "stage1 is %zu bytes; the BootROM's limit is 20 KiB (PM 34.9)", + stage1->size); + rc = JZ_ERR_BAD_FILE_FORMAT; + goto error; + } + + /* The second-stage image is linked at X1600_BOOT_LOAD_ADDR. */ + rc = jz_boot_get_file(dev->jz, &tar, "bootloader2.ucl", JZ_BOOT_DECOMPRESS, &bootloader); + if(rc != JZ_SUCCESS) + goto error; + + rc = jz_boot_get_file(dev->jz, &tar, "bootloader-info.txt", 0, &info_file); + if(rc != JZ_SUCCESS) + goto error; + + rc = jz_boot_show_version(dev->jz, info_file); + if(rc != JZ_SUCCESS) + goto error; + + /* Flush before upload so a previous stage1 is not executed from cache. + * Stage1 initialises DRAM and returns to the BootROM. */ + rc = jz_usb_flush_caches(dev); + if(rc != JZ_SUCCESS) + goto error; + + rc = jz_usb_send(dev, cpu_info->stage1_load_addr, stage1->size, stage1->data); + if(rc != JZ_SUCCESS) + goto error; + + rc = jz_usb_start1(dev, cpu_info->stage1_exec_addr); + if(rc != JZ_SUCCESS) + goto error; + + /* Give stage1 time to finish DRAM training and return. */ + jz_sleepms(500); + + rc = jz_usb_recv(dev, X1600_STAGE1_RESULT_ADDR, + sizeof(stage1_result), stage1_result); + if(rc != JZ_SUCCESS) + goto error; + + if(get_le32(stage1_result) != X1600_STAGE1_MAGIC) { + jz_log(dev->jz, JZ_LOG_ERROR, "Invalid stage1 result"); + rc = JZ_ERR_OTHER; + goto error; + } + + uint32_t status = get_le32(stage1_result + 4); + if(status != 0) { + jz_log(dev->jz, JZ_LOG_ERROR, + "Stage1 DDR initialization failed (status 0x%08lx)", + (unsigned long)status); + rc = JZ_ERR_OTHER; + goto error; + } + + /* Flush before uploading and executing the bootloader. */ + rc = jz_usb_flush_caches(dev); + if(rc != JZ_SUCCESS) + goto error; + + rc = jz_usb_send(dev, cpu_info->stage2_load_addr, + bootloader->size, bootloader->data); + if(rc != JZ_SUCCESS) + goto error; + + rc = jz_usb_flush_caches(dev); + if(rc != JZ_SUCCESS) + goto error; + + rc = jz_usb_start2(dev, cpu_info->stage2_exec_addr); + if(rc != JZ_SUCCESS) + goto error; + + rc = JZ_SUCCESS; + + error: + if(stage1) + jz_buffer_free(stage1); + if(bootloader) + jz_buffer_free(bootloader); + if(info_file) + jz_buffer_free(info_file); + mtar_close(&tar); + return rc; +} From 0a0b877dc29725df0fd2902b818a690cf10b0796 Mon Sep 17 00:00:00 2001 From: John Zimmermann Date: Wed, 16 Sep 2026 15:38:26 +0200 Subject: [PATCH 177/408] utils/ingenic_tools: correct return check of libusb_control_transfer in usbboot libusb_control_transfer returns <0 for errors, 0 is success without any bytes, read and ret >0 is the amount if bytes read. Tested command: sudo ./usbboot --vid 0xa108 --pid 0x1000 --cpuinfo Before: Can't get CPU info: 8 After: CPU info: X1000_v1 Change-Id: Ied6d430406239ea4f99e7c83274d99ae4c555899 --- utils/ingenic_tools/usbboot.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/ingenic_tools/usbboot.c b/utils/ingenic_tools/usbboot.c index 208888d3d4..e02941469c 100644 --- a/utils/ingenic_tools/usbboot.c +++ b/utils/ingenic_tools/usbboot.c @@ -103,7 +103,7 @@ void jz_get_cpu_info(void) int ret = libusb_control_transfer(g_usb_dev, LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE, VR_GET_CPU_INFO, 0, 0, buf, 8, 1000); - if(ret != 0) + if(ret < 0) die("Can't get CPU info: %d", ret); buf[8] = 0; @@ -172,7 +172,7 @@ void jz_download(const char* filename) int ret = libusb_control_transfer(g_usb_dev, \ LIBUSB_ENDPOINT_OUT|LIBUSB_REQUEST_TYPE_VENDOR|LIBUSB_RECIPIENT_DEVICE, \ VR_##type, param >> 16, param & 0xffff, NULL, 0, 1000); \ - if(ret != 0) \ + if(ret < 0) \ die("Request " #type " failed: %d", ret); \ } From 6958f6638e6d8ea28b26b5059adf394f00c42da6 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Thu, 17 Sep 2026 13:18:58 +1000 Subject: [PATCH 178/408] usb_storage: report read-only drives as write protected A target that defines HAVE_STORAGE_READONLY provides storage_readonly(), and USB mass storage reports such a drive write protected: MODE SENSE sets the WP bit, and WRITE(10) and WRITE(16) fail with DATA PROTECT / WRITE PROTECTED before taking data, so a host mounts it read-only instead of failing each write as a medium error. Only NAND storage wires it up so far. The iPod Nano 3G, added in the changes that follow, defines it: its FTL mounts read-only in the bootloader and after a failed commit, and the NAND check image is always read-only. Built for the Nano 3G (bootloader, check image, firmware) without new warnings. On hardware, Linux and Windows hosts have read the always-read-only check image over USB; no host write has been made to a read-only drive. AI provenance: developed with Claude Opus 5 (Anthropic), used through Claude Code. The model wrote most of the code and this message under Andrew Rice's direction. Any hardware testing described above was carried out by Andrew Rice, who is responsible for this change. Change-Id: Idb34def2d588047e5dced9bac1521c00b37dd5e5 --- firmware/usbstack/usb_storage.c | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/firmware/usbstack/usb_storage.c b/firmware/usbstack/usb_storage.c index 7f9057f643..6f10a087e1 100644 --- a/firmware/usbstack/usb_storage.c +++ b/firmware/usbstack/usb_storage.c @@ -127,6 +127,7 @@ #define SENSE_MEDIUM_ERROR 0x03 #define SENSE_ILLEGAL_REQUEST 0x05 #define SENSE_UNIT_ATTENTION 0x06 +#define SENSE_DATA_PROTECT 0x07 #define ASC_MEDIUM_NOT_PRESENT 0x3a #define ASC_INVALID_FIELD_IN_CBD 0x24 @@ -135,6 +136,14 @@ #define ASC_READ_ERROR 0x11 #define ASC_NOT_READY 0x04 #define ASC_INVALID_COMMAND 0x20 +#define ASC_WRITE_PROTECTED 0x27 + +/* A drive whose driver can only read it is reported write protected */ +#ifdef HAVE_STORAGE_READONLY +#define LUN_READONLY(lun) storage_readonly(lun) +#else +#define LUN_READONLY(lun) false +#endif #define ASCQ_BECOMING_READY 0x01 @@ -905,7 +914,8 @@ static void handle_scsi(struct command_block_wrapper* cbw) tb.ms_data_10->mode_data_length = htobe16(sizeof(struct mode_sense_data_10)-2); tb.ms_data_10->medium_type = 0; - tb.ms_data_10->device_specific = 0; + tb.ms_data_10->device_specific = + LUN_READONLY(lun) ? 0x80 : 0; tb.ms_data_10->reserved = 0; tb.ms_data_10->longlba = 1; tb.ms_data_10->block_descriptor_length = @@ -969,7 +979,8 @@ static void handle_scsi(struct command_block_wrapper* cbw) tb.ms_data_6->mode_data_length = sizeof(struct mode_sense_data_6)-1; tb.ms_data_6->medium_type = 0; - tb.ms_data_6->device_specific = 0; + tb.ms_data_6->device_specific = + LUN_READONLY(lun) ? 0x80 : 0; tb.ms_data_6->block_descriptor_length = sizeof(struct mode_sense_bdesc_shortlba); tb.ms_data_6->block_descriptor.density_code = 0; @@ -1249,6 +1260,13 @@ static void handle_scsi(struct command_block_wrapper* cbw) cur_sense_data.ascq=0; break; } + if(LUN_READONLY(lun)) { + send_csw(UMS_STATUS_FAIL); + cur_sense_data.sense_key=SENSE_DATA_PROTECT; + cur_sense_data.asc=ASC_WRITE_PROTECTED; + cur_sense_data.ascq=0; + break; + } cur_cmd.data[0] = tb.transfer_buffer; cur_cmd.data[1] = &tb.transfer_buffer[WRITE_BUFFER_SIZE]; cur_cmd.data_select=0; @@ -1284,6 +1302,13 @@ static void handle_scsi(struct command_block_wrapper* cbw) cur_sense_data.ascq=0; break; } + if(LUN_READONLY(lun)) { + send_csw(UMS_STATUS_FAIL); + cur_sense_data.sense_key=SENSE_DATA_PROTECT; + cur_sense_data.asc=ASC_WRITE_PROTECTED; + cur_sense_data.ascq=0; + break; + } cur_cmd.data[0] = tb.transfer_buffer; cur_cmd.data[1] = &tb.transfer_buffer[WRITE_BUFFER_SIZE]; cur_cmd.data_select=0; From 3a57f2f7215a3c55ed52b121f564b60159a8a42f Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Thu, 3 Sep 2026 20:35:48 +0100 Subject: [PATCH 179/408] Add "rbfs" prefix to native filesystem functions Most of the churn here occurs because 'filesize' is one of the redefined filesystem functions, but some of the structs used by the native FS code also include a 'filesize' member variable which gets renamed by the macro in some but not all source files. It's easier to rename 'filesize()' to 'ffilesize()' rather than try to clean up the macro mess or renaming the struct members. There is weirdness with root_realpath() which now breaks on native builds because it was assumed to be unprefixed there. dir_get_info() was unprefixed everywhere but this just seems inconsistent; make it follow the FS_PREFIX() convention too. Change-Id: Ic3700c6234ea45f32679c1a8429d70fdb8f4088a --- apps/bookmark.c | 2 +- apps/buffering.c | 2 +- apps/core_keymap.c | 2 +- apps/filetypes.c | 2 +- apps/gui/skin_engine/skin_tokens.c | 2 +- apps/playlist.c | 2 +- apps/playlist_catalog.c | 2 +- apps/plugin.c | 4 +- apps/plugin.h | 2 +- apps/plugins/bench_mem_jpeg.c | 2 +- apps/plugins/clock/clock_settings.c | 2 +- apps/plugins/crypt_firmware.c | 2 +- apps/plugins/dict.c | 2 +- apps/plugins/doom/d_deh.c | 2 +- apps/plugins/doom/m_misc.c | 2 +- apps/plugins/doom/rockmacros.h | 2 +- apps/plugins/doom/w_wad.c | 2 +- apps/plugins/imageviewer/jpeg/jpeg.c | 2 +- apps/plugins/imageviewer/png/png.c | 2 +- apps/plugins/imageviewer/ppm/ppm.c | 2 +- apps/plugins/iriver_flash.c | 2 +- apps/plugins/keyremap.c | 2 +- apps/plugins/lastfm_scrobbler_viewer.c | 2 +- apps/plugins/lrcplayer.c | 2 +- apps/plugins/lua/liolib.c | 2 +- apps/plugins/lua/rocklibc.h | 4 +- apps/plugins/mikmod/mikmod_supp.h | 4 +- apps/plugins/mikmod/mmio.c | 2 +- apps/plugins/mp3_encoder.c | 2 +- apps/plugins/mpegplayer/disk_buf.c | 2 +- apps/plugins/open_plugins.c | 2 +- apps/plugins/pdbox/PDa/intern/sfread~.c | 2 +- apps/plugins/pictureflow/pictureflow.c | 2 +- apps/plugins/rockboy/rockboy.c | 2 +- apps/plugins/tagcache/tagcache.c | 2 +- apps/plugins/test_codec.c | 4 +- apps/plugins/test_mem_jpeg.c | 2 +- apps/plugins/text_editor.c | 2 +- apps/plugins/text_viewer/tv_reader.c | 2 +- apps/recorder/jpeg_load.c | 2 +- apps/tagcache.c | 2 +- apps/talk.c | 2 +- bootloader/common.c | 2 +- bootloader/gigabeat-s.c | 4 +- bootloader/x1000/boot.c | 2 +- firmware/common/dir.c | 22 +++---- firmware/common/dircache.c | 2 +- firmware/common/file.c | 32 +++++----- firmware/common/rb-loader.c | 2 +- firmware/common/unicode.c | 2 +- firmware/common/zip.c | 4 +- firmware/font.c | 4 +- firmware/include/dir.h | 11 +--- firmware/include/dircache_redirect.h | 2 +- firmware/include/file.h | 4 +- firmware/include/filesystem-native.h | 59 ++++++++++--------- firmware/lc-rock.c | 2 +- firmware/target/hosted/filesystem-app.c | 2 +- firmware/target/hosted/filesystem-app.h | 4 +- firmware/target/hosted/filesystem-console.h | 5 +- firmware/target/hosted/filesystem-hosted.h | 2 +- firmware/target/hosted/filesystem-unix.c | 2 +- firmware/target/hosted/filesystem-win32.h | 4 +- .../hosted/game_console/lib/sys_dir_3ds.c | 2 +- .../hosted/game_console/lib/sys_file_common.c | 4 +- lib/rbcodec/codecs/librm/rm.c | 2 +- lib/rbcodec/codecs/librm/rm.h | 2 +- lib/rbcodec/metadata/a52.c | 2 +- lib/rbcodec/metadata/aac.c | 2 +- lib/rbcodec/metadata/adx.c | 2 +- lib/rbcodec/metadata/aiff.c | 2 +- lib/rbcodec/metadata/asap.c | 2 +- lib/rbcodec/metadata/asf.c | 2 +- lib/rbcodec/metadata/au.c | 2 +- lib/rbcodec/metadata/ay.c | 2 +- lib/rbcodec/metadata/flac.c | 2 +- lib/rbcodec/metadata/gbs.c | 2 +- lib/rbcodec/metadata/hes.c | 2 +- lib/rbcodec/metadata/kss.c | 2 +- lib/rbcodec/metadata/metadata.c | 4 +- lib/rbcodec/metadata/mod.c | 4 +- lib/rbcodec/metadata/monkeys.c | 2 +- lib/rbcodec/metadata/mp3.c | 2 +- lib/rbcodec/metadata/mp3data.c | 2 +- lib/rbcodec/metadata/mp4.c | 2 +- lib/rbcodec/metadata/mpc.c | 2 +- lib/rbcodec/metadata/nsf.c | 2 +- lib/rbcodec/metadata/ogg.c | 2 +- lib/rbcodec/metadata/oma.c | 2 +- lib/rbcodec/metadata/rm.c | 4 +- lib/rbcodec/metadata/sgc.c | 2 +- lib/rbcodec/metadata/sid.c | 2 +- lib/rbcodec/metadata/smaf.c | 2 +- lib/rbcodec/metadata/spc.c | 2 +- lib/rbcodec/metadata/tta.c | 2 +- lib/rbcodec/metadata/vgm.c | 2 +- lib/rbcodec/metadata/vox.c | 2 +- lib/rbcodec/metadata/vtx.c | 4 +- lib/rbcodec/metadata/wave.c | 2 +- lib/rbcodec/metadata/wavpack.c | 2 +- lib/rbcodec/rbcodecplatform-unix.h | 2 +- lib/rbcodec/test/file.h | 2 +- lib/rbcodec/test/warble.c | 4 +- uisimulator/common/filesystem-sim.c | 8 +-- uisimulator/common/filesystem-sim.h | 5 +- 105 files changed, 188 insertions(+), 182 deletions(-) diff --git a/apps/bookmark.c b/apps/bookmark.c index 8c4cb18211..a8a0961c8a 100644 --- a/apps/bookmark.c +++ b/apps/bookmark.c @@ -692,7 +692,7 @@ static int buffer_bookmarks(struct bookmark_list* bookmarks, int first_line) return -1; } - if ((first_line != 0) && ((size_t) filesize(file) < bookmarks->buffer_size + if ((first_line != 0) && ((size_t) ffilesize(file) < bookmarks->buffer_size - sizeof(*bookmarks) - (sizeof(char*) * bookmarks->total_count))) { /* Entire file fits in buffer */ diff --git a/apps/buffering.c b/apps/buffering.c index a4c179af41..8b9617e98f 100644 --- a/apps/buffering.c +++ b/apps/buffering.c @@ -982,7 +982,7 @@ int bufopen(const char *file, off_t offset, enum data_type type, #endif /* HAVE_ALBUMART */ if (size == 0) - size = filesize(fd); + size = ffilesize(fd); unsigned int hflags = 0; if (type == TYPE_PACKET_AUDIO || type == TYPE_CODEC) diff --git a/apps/core_keymap.c b/apps/core_keymap.c index d9fa331727..cc7e00c428 100644 --- a/apps/core_keymap.c +++ b/apps/core_keymap.c @@ -37,7 +37,7 @@ static int open_key_remap(const char *filename, int *countp) if (fd < 0) return fd; - size_t fsize = filesize(fd); + size_t fsize = ffilesize(fd); int count = fsize / sizeof(struct button_mapping); if (count == 0 || (size_t)(count * sizeof(struct button_mapping)) != fsize) { diff --git a/apps/filetypes.c b/apps/filetypes.c index 4c68fc9579..5578026bbf 100644 --- a/apps/filetypes.c +++ b/apps/filetypes.c @@ -382,7 +382,7 @@ static void read_viewers_config_init(void) if(fd < 0) return; - off_t filesz = filesize(fd); + off_t filesz = ffilesize(fd); if(filesz <= 0) goto out; diff --git a/apps/gui/skin_engine/skin_tokens.c b/apps/gui/skin_engine/skin_tokens.c index cab4385bb9..ecf2a9a4fc 100644 --- a/apps/gui/skin_engine/skin_tokens.c +++ b/apps/gui/skin_engine/skin_tokens.c @@ -290,7 +290,7 @@ void wps_playlist_percent_prepare(void) if (fd >= 0) { unsigned int afmt = probe_file_format(info.filename); - off_t size = filesize(fd); + off_t size = ffilesize(fd); if (size > 0) { diff --git a/apps/playlist.c b/apps/playlist.c index 9140d25a8c..294e2093b3 100644 --- a/apps/playlist.c +++ b/apps/playlist.c @@ -3218,7 +3218,7 @@ int playlist_resume(void) } playlist->control_created = true; - control_file_size = filesize(playlist->control_fd); + control_file_size = ffilesize(playlist->control_fd); if (control_file_size <= 0) { notify_control_access_error(); diff --git a/apps/playlist_catalog.c b/apps/playlist_catalog.c index 7a7e082e21..eee746d0f9 100644 --- a/apps/playlist_catalog.c +++ b/apps/playlist_catalog.c @@ -325,7 +325,7 @@ int catalog_insert_into(const char* playlist, bool new_playlist, goto exit; i = lseek(f, 0, SEEK_CUR); - fs = filesize(f); + fs = ffilesize(f); while (i < fs) { int n; diff --git a/apps/plugin.c b/apps/plugin.c index 7f517fe727..83574d5ed0 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -430,7 +430,7 @@ static const struct plugin_api rockbox_api = { FS_PREFIX(remove), FS_PREFIX(rename), FS_PREFIX(ftruncate), - FS_PREFIX(filesize), + FS_PREFIX(ffilesize), fdprintf, read_line, settings_parseline, @@ -460,7 +460,7 @@ static const struct plugin_api rockbox_api = { FS_PREFIX(mkdir), FS_PREFIX(rmdir), FS_PREFIX(dir_exists), - dir_get_info, + FS_PREFIX(dir_get_info), /* browsing */ rockbox_browse, diff --git a/apps/plugin.h b/apps/plugin.h index 0cbf638444..e98c4b78a3 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -498,7 +498,7 @@ struct plugin_api { int (*remove)(const char *path); int (*rename)(const char *old, const char *new); int (*ftruncate)(int fildes, off_t length); - off_t (*filesize)(int fildes); + off_t (*ffilesize)(int fildes); int (*fdprintf)(int fildes, const char *fmt, ...) ATTRIBUTE_PRINTF(2, 3); int (*read_line)(int fd, char* buffer, int buffer_size); bool (*settings_parseline)(char* line, char** name, char** value); diff --git a/apps/plugins/bench_mem_jpeg.c b/apps/plugins/bench_mem_jpeg.c index db210613b8..25db2d1923 100644 --- a/apps/plugins/bench_mem_jpeg.c +++ b/apps/plugins/bench_mem_jpeg.c @@ -89,7 +89,7 @@ enum plugin_status plugin_start(const void* parameter) lcd_printf("file open failed: %d", fd); goto wait; } - unsigned long filesize = rb->filesize(fd); + unsigned long filesize = rb->ffilesize(fd); if (filesize > plugin_buf_len) { lcd_printf("file too large"); diff --git a/apps/plugins/clock/clock_settings.c b/apps/plugins/clock/clock_settings.c index 6e6f32bd36..e5a3c61cf0 100644 --- a/apps/plugins/clock/clock_settings.c +++ b/apps/plugins/clock/clock_settings.c @@ -117,7 +117,7 @@ static enum settings_file_status clock_settings_load( if(fd >= 0) { /* basic consistency check */ - if(rb->filesize(fd) != sizeof(*settings)) + if(rb->ffilesize(fd) != sizeof(*settings)) rb->close(fd); else { diff --git a/apps/plugins/crypt_firmware.c b/apps/plugins/crypt_firmware.c index 332d18e761..945781cdea 100644 --- a/apps/plugins/crypt_firmware.c +++ b/apps/plugins/crypt_firmware.c @@ -150,7 +150,7 @@ enum plugin_status plugin_start(const void* parameter) return PLUGIN_ERROR; } - length = rb->filesize(fd); + length = rb->ffilesize(fd); if (length < 12) { rb->splash(HZ*2, "File too small"); diff --git a/apps/plugins/dict.c b/apps/plugins/dict.c index fe61229c7e..c679914b9b 100644 --- a/apps/plugins/dict.c +++ b/apps/plugins/dict.c @@ -102,7 +102,7 @@ enum plugin_status plugin_start(const void* parameter) return PLUGIN_ERROR; } - filesize = rb->filesize(fIndex); /* get filesize */ + filesize = rb->ffilesize(fIndex); /* get filesize */ DEBUGF("Filesize: %d bytes = %d words \n", filesize, (filesize / (int)sizeof(struct stWord))); diff --git a/apps/plugins/doom/d_deh.c b/apps/plugins/doom/d_deh.c index 92ea91b6ae..f8eae5af01 100644 --- a/apps/plugins/doom/d_deh.c +++ b/apps/plugins/doom/d_deh.c @@ -106,7 +106,7 @@ int dehfeof(DEHFILE *fp) { if (fp->fd >= 0) { - off_t size = filesize(fp->fd); + off_t size = ffilesize(fp->fd); off_t offset = lseek(fp->fd, 0, SEEK_CUR); return (size <= 0 || offset < 0 || offset >= size) ? 1 : 0; } diff --git a/apps/plugins/doom/m_misc.c b/apps/plugins/doom/m_misc.c index 0318c9d741..9d1d33ffcf 100644 --- a/apps/plugins/doom/m_misc.c +++ b/apps/plugins/doom/m_misc.c @@ -127,7 +127,7 @@ int M_ReadFile(char const* name,byte** buffer) if ((handle < 0)) I_Error ("M_ReadFile: Couldn't read file %s", name); - length = filesize(handle); + length = ffilesize(handle); buf = Z_Malloc (length, PU_STATIC, NULL); count = read (handle, buf, length); close (handle); diff --git a/apps/plugins/doom/rockmacros.h b/apps/plugins/doom/rockmacros.h index d46d5a22fc..7f01cf2f95 100644 --- a/apps/plugins/doom/rockmacros.h +++ b/apps/plugins/doom/rockmacros.h @@ -52,7 +52,7 @@ int my_close(int id); #endif #define lseek(a,b,c) rb->lseek((a),(b),(c)) -#define filesize(a) rb->filesize((a)) +#define ffilesize(a) rb->ffilesize((a)) #define read(a,b,c) rb->read((a),(b),(c)) #define write(a,b,c) rb->write((a),(b),(c)) #undef strtok diff --git a/apps/plugins/doom/w_wad.c b/apps/plugins/doom/w_wad.c index 48d695c3b4..3b262d62f8 100644 --- a/apps/plugins/doom/w_wad.c +++ b/apps/plugins/doom/w_wad.c @@ -87,7 +87,7 @@ static int W_Filelength(int handle) I_Error("W_Filelength: Error fstating"); return fileinfo.st_size; */ - return filesize(handle); + return ffilesize(handle); } void ExtractFileBase (const char *path, char *dest) diff --git a/apps/plugins/imageviewer/jpeg/jpeg.c b/apps/plugins/imageviewer/jpeg/jpeg.c index e8d654c00b..2436e90f68 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg.c +++ b/apps/plugins/imageviewer/jpeg/jpeg.c @@ -133,7 +133,7 @@ static int load_image(char *filename, struct image_info *info, } else { - filesize = rb->filesize(fd); + filesize = rb->ffilesize(fd); } /* allocate JPEG buffer */ diff --git a/apps/plugins/imageviewer/png/png.c b/apps/plugins/imageviewer/png/png.c index 5f9aa9393c..5fb999d060 100644 --- a/apps/plugins/imageviewer/png/png.c +++ b/apps/plugins/imageviewer/png/png.c @@ -120,7 +120,7 @@ static int load_image(char *filename, struct image_info *info, } else { - file_size = rb->filesize(fd); + file_size = rb->ffilesize(fd); } DEBUGF("reading file '%s'\n", filename); diff --git a/apps/plugins/imageviewer/ppm/ppm.c b/apps/plugins/imageviewer/ppm/ppm.c index f2bdc07d68..e1ae1b278d 100644 --- a/apps/plugins/imageviewer/ppm/ppm.c +++ b/apps/plugins/imageviewer/ppm/ppm.c @@ -106,7 +106,7 @@ static int load_image(char *filename, struct image_info *info, } else { - filesize = rb->filesize(fd); + filesize = rb->ffilesize(fd); } DEBUGF("reading file '%s'\n", filename); diff --git a/apps/plugins/iriver_flash.c b/apps/plugins/iriver_flash.c index a67b793607..1ad222bf40 100644 --- a/apps/plugins/iriver_flash.c +++ b/apps/plugins/iriver_flash.c @@ -373,7 +373,7 @@ static bool load_firmware(const char* filename, enum firmware firmware, } /* get file and buffer lengths and acquire the buffer */ - fd_len = rb->filesize(fd); + fd_len = rb->ffilesize(fd); buffer = rb->plugin_get_audio_buffer(&buffer_len); /* ensure there's enough space in the buffer */ diff --git a/apps/plugins/keyremap.c b/apps/plugins/keyremap.c index a224cc292a..04ef074ff6 100644 --- a/apps/plugins/keyremap.c +++ b/apps/plugins/keyremap.c @@ -374,7 +374,7 @@ static int keyremap_open_file(const char *filename, int *fd, size_t *fsize) *fd = rb->open(filename, O_RDONLY); if (*fd) { - *fsize = rb->filesize(*fd); + *fsize = rb->ffilesize(*fd); count = *fsize / sizeof(struct button_mapping); diff --git a/apps/plugins/lastfm_scrobbler_viewer.c b/apps/plugins/lastfm_scrobbler_viewer.c index 8ce5ab8b85..c138d4976e 100644 --- a/apps/plugins/lastfm_scrobbler_viewer.c +++ b/apps/plugins/lastfm_scrobbler_viewer.c @@ -200,7 +200,7 @@ static size_t file_open(const char *filename, int *fd) *fd = rb->open(filename, O_RDONLY); if (*fd >= 0) { - fsize = rb->filesize(*fd); + fsize = rb->ffilesize(*fd); } } return fsize; diff --git a/apps/plugins/lrcplayer.c b/apps/plugins/lrcplayer.c index 8ce00fad7a..7468e24f08 100644 --- a/apps/plugins/lrcplayer.c +++ b/apps/plugins/lrcplayer.c @@ -1944,7 +1944,7 @@ static void save_changes(void) rb->write(fd, BOM, BOM_SIZE); } else - size = rb->filesize(fe); + size = rb->ffilesize(fe); while (curr < size) { /* find offset of next tag */ diff --git a/apps/plugins/lua/liolib.c b/apps/plugins/lua/liolib.c index 22213191a5..e8b3f9106f 100644 --- a/apps/plugins/lua/liolib.c +++ b/apps/plugins/lua/liolib.c @@ -267,7 +267,7 @@ static int read_number (lua_State *L, int *f) { static int test_eof (lua_State *L, int *f) { off_t s = lseek(*f, 0, SEEK_CUR); lua_pushlstring(L, NULL, 0); - return s < filesize(*f); + return s < ffilesize(*f); } diff --git a/apps/plugins/lua/rocklibc.h b/apps/plugins/lua/rocklibc.h index 3dd093d2f3..eb34e1f147 100644 --- a/apps/plugins/lua/rocklibc.h +++ b/apps/plugins/lua/rocklibc.h @@ -52,7 +52,7 @@ extern int errno; #define open(...) rb->open(__VA_ARGS__) #define close(fd) rb->close(fd) #define lseek(fd,off,w) rb->lseek(fd,off,w) -#define filesize(fd) rb->filesize(fd) +#define ffilesize(fd) rb->ffilesize(fd) #define ftruncate(fd,l) rb->ftruncate(fd,l) #define file_exists(p) rb->file_exists(p) #define remove(f) rb->remove(f) @@ -71,7 +71,7 @@ extern int errno; #define open(...) PREFIX(open)(__VA_ARGS__) #define close(fd) PREFIX(close)(fd) #define lseek(fd,off,w) PREFIX(lseek)(fd,off,w) -#define filesize(fd) PREFIX(filesize)(fd) +#define ffilesize(fd) PREFIX(ffilesize)(fd) #define ftruncate(fd,l) PREFIX(ftruncate)(fd,l) #define file_exists(p) PREFIX(file_exists)(p) #define remove(f) PREFIX(remove)(f) diff --git a/apps/plugins/mikmod/mikmod_supp.h b/apps/plugins/mikmod/mikmod_supp.h index f7d8d26d2a..a82670bf15 100644 --- a/apps/plugins/mikmod/mikmod_supp.h +++ b/apps/plugins/mikmod/mikmod_supp.h @@ -46,8 +46,8 @@ #define read(a,b,c) rb->read(a,b,c) #undef write #define write(a,b,c) rb->write(a,b,c) -#undef filesize -#define filesize(a) rb->filesize(a) +#undef ffilesize +#define ffilesize(a) rb->ffilesize(a) #endif #define malloc(x) tlsf_malloc(x) diff --git a/apps/plugins/mikmod/mmio.c b/apps/plugins/mikmod/mmio.c index 6e317ca008..ef3d44d756 100644 --- a/apps/plugins/mikmod/mmio.c +++ b/apps/plugins/mikmod/mmio.c @@ -133,7 +133,7 @@ typedef struct MFILEREADER { static int _mm_FileReader_Eof(MREADER* reader) { //return feof(((MFILEREADER*)reader)->file); - int size = filesize(((MFILEREADER*)reader)->file); + int size = ffilesize(((MFILEREADER*)reader)->file); int offset = lseek(((MFILEREADER*)reader)->file, 0, SEEK_CUR); return (size <= 0 || offset < 0 || offset >= size) ? 1 : 0; } diff --git a/apps/plugins/mp3_encoder.c b/apps/plugins/mp3_encoder.c index 9a8acecf88..5d3c525e8c 100644 --- a/apps/plugins/mp3_encoder.c +++ b/apps/plugins/mp3_encoder.c @@ -906,7 +906,7 @@ static int wave_open(void) (cfg.samplerate != 44100) && (cfg.samplerate != 48000)) return -9; header_size = 0x28; - wav_size = rb->filesize(wavfile); + wav_size = rb->ffilesize(wavfile); rb->lseek(wavfile, header_size, SEEK_SET); return 0; diff --git a/apps/plugins/mpegplayer/disk_buf.c b/apps/plugins/mpegplayer/disk_buf.c index 9f55f57ee0..1d1d1695a9 100644 --- a/apps/plugins/mpegplayer/disk_buf.c +++ b/apps/plugins/mpegplayer/disk_buf.c @@ -875,7 +875,7 @@ int disk_buf_open(const char *filename) if (fd >= 0) { - ssize_t filesize = rb->filesize(fd); + ssize_t filesize = rb->ffilesize(fd); if (filesize <= 0) { diff --git a/apps/plugins/open_plugins.c b/apps/plugins/open_plugins.c index 1a10c797e9..84a2187f88 100644 --- a/apps/plugins/open_plugins.c +++ b/apps/plugins/open_plugins.c @@ -191,7 +191,7 @@ static int op_entry_read_opx(const char *path) fd_opx = rb->open(path, O_RDONLY); if (fd_opx >= 0) { - filesize = rb->filesize(fd_opx); + filesize = rb->ffilesize(fd_opx); ret = filesize; if (filesize == op_entry_sz && !op_entry_read(fd_opx, 0, op_entry_sz)) ret = 0; diff --git a/apps/plugins/pdbox/PDa/intern/sfread~.c b/apps/plugins/pdbox/PDa/intern/sfread~.c index 0a369ce3f5..1da54e07cd 100644 --- a/apps/plugins/pdbox/PDa/intern/sfread~.c +++ b/apps/plugins/pdbox/PDa/intern/sfread~.c @@ -78,7 +78,7 @@ void sfread_open(t_sfread *x,t_symbol *filename) /* get the size */ #ifdef ROCKBOX - x->x_size = rb->filesize(x->x_fd); + x->x_size = rb->ffilesize(x->x_fd); #else fstat(x->x_fd,&fstate); x->x_size = fstate.st_size; diff --git a/apps/plugins/pictureflow/pictureflow.c b/apps/plugins/pictureflow/pictureflow.c index 872fbaa2ac..bba1a5d70a 100644 --- a/apps/plugins/pictureflow/pictureflow.c +++ b/apps/plugins/pictureflow/pictureflow.c @@ -1584,7 +1584,7 @@ static int load_album_index(void){ int album_idx, artist_idx; if (fr >= 0){ - const unsigned long filesize = rb->filesize(fr); + const unsigned long filesize = rb->ffilesize(fr); if (filesize > sizeof(data)) { if (rb->read(fr, &data, sizeof(data)) == sizeof(data) && diff --git a/apps/plugins/rockboy/rockboy.c b/apps/plugins/rockboy/rockboy.c index 3c4bcb829d..fb822996e3 100644 --- a/apps/plugins/rockboy/rockboy.c +++ b/apps/plugins/rockboy/rockboy.c @@ -74,7 +74,7 @@ static void setoptions (void) int optionssize = sizeof(options); int filesize = 0; if(fd >= 0) - filesize = rb->filesize(fd); + filesize = rb->ffilesize(fd); /* don't read the option file if the size * is not as expected to avoid crash */ diff --git a/apps/plugins/tagcache/tagcache.c b/apps/plugins/tagcache/tagcache.c index 58d263da7b..602e366793 100644 --- a/apps/plugins/tagcache/tagcache.c +++ b/apps/plugins/tagcache/tagcache.c @@ -54,7 +54,7 @@ static bool logdump(bool append); #define vsnprintf rb->vsnprintf #define mkdir rb->mkdir -#define filesize rb->filesize +#define ffilesize rb->ffilesize #define strtok_r rb->strtok_r #define strncasecmp rb->strncasecmp diff --git a/apps/plugins/test_codec.c b/apps/plugins/test_codec.c index ba5b264372..7bd69369cc 100644 --- a/apps/plugins/test_codec.c +++ b/apps/plugins/test_codec.c @@ -181,7 +181,7 @@ void init_wav(char* filename) void close_wav(void) { - int filesize = rb->filesize(wavinfo.fd); + int filesize = rb->ffilesize(wavinfo.fd); int channels = (wavinfo.stereomode == STEREO_MONO) ? 1 : 2; int bps = 16; /* TODO */ @@ -658,7 +658,7 @@ static enum plugin_status test_track(const char* filename) goto exit; } - track.filesize = rb->filesize(fd); + track.filesize = rb->ffilesize(fd); /* Clear the id3 struct */ rb->memset(&track.id3, 0, sizeof(struct mp3entry)); diff --git a/apps/plugins/test_mem_jpeg.c b/apps/plugins/test_mem_jpeg.c index 855bf71677..e92d9d1d72 100644 --- a/apps/plugins/test_mem_jpeg.c +++ b/apps/plugins/test_mem_jpeg.c @@ -66,7 +66,7 @@ enum plugin_status plugin_start(const void* parameter) int fd = rb->open(filename, O_RDONLY); if (fd < 0) return PLUGIN_ERROR; - unsigned long filesize = rb->filesize(fd); + unsigned long filesize = rb->ffilesize(fd); if (filesize > plugin_buf_len) return PLUGIN_ERROR; plugin_buf_len -= filesize; diff --git a/apps/plugins/text_editor.c b/apps/plugins/text_editor.c index 2fcdb90b9a..6b7dee868f 100644 --- a/apps/plugins/text_editor.c +++ b/apps/plugins/text_editor.c @@ -413,7 +413,7 @@ enum plugin_status plugin_start(const void* parameter) if (c && !rb->strcmp(c, ".colours")) edit_colors_file = true; #endif - if (buffer_size <= (size_t)rb->filesize(fd) + 0x400) + if (buffer_size <= (size_t)rb->ffilesize(fd) + 0x400) { buffer = rb->plugin_get_audio_buffer(&buffer_size); audio_buf = true; diff --git a/apps/plugins/text_viewer/tv_reader.c b/apps/plugins/text_viewer/tv_reader.c index d4f9040609..70448582d4 100644 --- a/apps/plugins/text_viewer/tv_reader.c +++ b/apps/plugins/text_viewer/tv_reader.c @@ -144,7 +144,7 @@ static int tv_change_preferences(const struct tv_preferences *oldp) if (fd < 0) return TV_CALLBACK_ERROR; - file_size = rb->filesize(fd); + file_size = rb->ffilesize(fd); change_file = true; } diff --git a/apps/recorder/jpeg_load.c b/apps/recorder/jpeg_load.c index 3214cc9763..c85d9965b4 100644 --- a/apps/recorder/jpeg_load.c +++ b/apps/recorder/jpeg_load.c @@ -2085,7 +2085,7 @@ int clip_jpeg_fd(int fd, int flags, #else p_jpeg->fd = fd; if (p_jpeg->len == 0) - p_jpeg->len = filesize(p_jpeg->fd); + p_jpeg->len = ffilesize(p_jpeg->fd); p_jpeg->read_buf = read_buf; p_jpeg->skip_bytes_seek = skip_bytes_seek; diff --git a/apps/tagcache.c b/apps/tagcache.c index d5c8f79a7a..e33bc9330e 100644 --- a/apps/tagcache.c +++ b/apps/tagcache.c @@ -3096,7 +3096,7 @@ static int build_index(int index_type, struct tagcache_header *h, int tmpfd) */ masterfd_pos = lseek(masterfd, tcmh.tch.entry_count * sizeof(struct index_entry), SEEK_CUR); - if (masterfd_pos == filesize(masterfd)) + if (masterfd_pos == ffilesize(masterfd)) { logf("appending..."); init = true; diff --git a/apps/talk.c b/apps/talk.c index 9aaa93e70f..a353eadc6c 100644 --- a/apps/talk.c +++ b/apps/talk.c @@ -1117,7 +1117,7 @@ static int _talk_file(const char* filename, { return 0; } - size = filesize(fd); + size = ffilesize(fd); if (size > 0) { diff --git a/bootloader/common.c b/bootloader/common.c index 705441357a..b07d419861 100644 --- a/bootloader/common.c +++ b/bootloader/common.c @@ -153,7 +153,7 @@ int load_raw_firmware(unsigned char* buf, char* firmware, int buffer_size) return EFILE_NOT_FOUND; } - len = filesize(fd); + len = ffilesize(fd); if (len > buffer_size) return EFILE_TOO_BIG; diff --git a/bootloader/gigabeat-s.c b/bootloader/gigabeat-s.c index b0c9b54f4d..42943111be 100644 --- a/bootloader/gigabeat-s.c +++ b/bootloader/gigabeat-s.c @@ -151,7 +151,7 @@ static void untar(int tar_fd) char path[102]; int fd, i; int ret; - size_t size = filesize(tar_fd); + size_t size = ffilesize(tar_fd); if (size > tarbuf_size) { @@ -159,7 +159,7 @@ static void untar(int tar_fd) return; } - ret = read(tar_fd, tarbuf, filesize(tar_fd)); + ret = read(tar_fd, tarbuf, ffilesize(tar_fd)); if (ret < 0) { printf("couldn't read tar file (%d)", ret); diff --git a/bootloader/x1000/boot.c b/bootloader/x1000/boot.c index e196159617..66fd62858a 100644 --- a/bootloader/x1000/boot.c +++ b/bootloader/x1000/boot.c @@ -85,7 +85,7 @@ static int read_linux_args(const char* filename) } /* this isn't 100% correct but will be good enough */ - off_t fsize = filesize(fd); + off_t fsize = ffilesize(fd); if(fsize < 0 || fsize+1 > (off_t)max_size) { splashf(5*HZ, "Arguments too long"); ret = -4; diff --git a/firmware/common/dir.c b/firmware/common/dir.c index 1f04c4f46b..2e0878d0ab 100644 --- a/firmware/common/dir.c +++ b/firmware/common/dir.c @@ -104,7 +104,7 @@ static struct dirstr_desc * alloc_dirstr(void) /** POSIX interface **/ /* open a directory */ -DIR * opendir(const char *dirname) +DIR * rbfs_opendir(const char *dirname) { DEBUGF("opendir(dirname=\"%s\"\n", dirname); @@ -132,7 +132,7 @@ file_error: } /* close a directory stream */ -int closedir(DIR *dirp) +int rbfs_closedir(DIR *dirp) { int rc; @@ -159,7 +159,7 @@ file_error: } /* read a directory */ -struct dirent * readdir(DIR *dirp) +struct dirent * rbfs_readdir(DIR *dirp) { struct dirstr_desc * const dir = GET_DIRSTR(READER, dirp); if (!dir) @@ -184,7 +184,7 @@ file_error: #if 0 /* not included now but probably should be */ /* read a directory (reentrant) */ -int readdir_r(DIR *dirp, struct dirent *entry, struct dirent **result) +int rbfs_readdir_r(DIR *dirp, struct dirent *entry, struct dirent **result) { if (!result) FILE_ERROR_RETURN(EFAULT, -2); @@ -218,7 +218,7 @@ file_error: } /* reset the position of a directory stream to the beginning of a directory */ -void rewinddir(DIR *dirp) +void rbfs_rewinddir(DIR *dirp) { struct dirstr_desc * const dir = GET_DIRSTR(READER, dirp); if (!dir) @@ -232,7 +232,7 @@ void rewinddir(DIR *dirp) #endif /* 0 */ /* make a directory */ -int mkdir(const char *path) +int rbfs_mkdir(const char *path) { DEBUGF("mkdir(path=\"%s\")\n", path); @@ -269,7 +269,7 @@ file_error: } /* remove a directory */ -int rmdir(const char *name) +int rbfs_rmdir(const char *name) { DEBUGF("rmdir(name=\"%s\")\n", name); @@ -295,7 +295,7 @@ int rmdir(const char *name) /** Extended interface **/ /* return if two directory streams refer to the same directory */ -int samedir(DIR *dirp1, DIR *dirp2) +int rbfs_samedir(DIR *dirp1, DIR *dirp2) { struct dirstr_desc * const dir1 = GET_DIRSTR(WRITER, dirp1); if (!dir1) @@ -312,7 +312,7 @@ int samedir(DIR *dirp1, DIR *dirp2) } /* test directory existence (returns 'false' if a file) */ -bool dir_exists(const char *dirname) +bool rbfs_dir_exists(const char *dirname) { file_internal_lock_WRITER(); bool rc = test_stream_exists_internal(dirname, FF_DIR) > 0; @@ -321,7 +321,7 @@ bool dir_exists(const char *dirname) } /* get the portable info from the native entry */ -struct dirinfo dir_get_info(DIR *dirp, struct dirent *entry) +struct dirinfo rbfs_dir_get_info(DIR *dirp, struct dirent *entry) { int rc; if (!dirp || !entry) @@ -344,7 +344,7 @@ file_error: return (struct dirinfo){ .attribute = 0 }; } -const char* root_realpath(void) +const char* rbfs_root_realpath(void) { /* Native only, for APP and SIM see respective filesystem-.c files */ return root_get_realpath(); /* rb_namespace.c */ diff --git a/firmware/common/dircache.c b/firmware/common/dircache.c index 1464ffab79..6a91b6cb5d 100644 --- a/firmware/common/dircache.c +++ b/firmware/common/dircache.c @@ -3060,7 +3060,7 @@ int INIT_ATTR dircache_load(void) if (maindata.dircache.size != maindata.dircache.sizeentries + maindata.dircache.sizenames || ALIGN_DOWN(maindata.dircache.size, ENTRYSIZE) != maindata.dircache.size || - filesize(fd) - sizeof (maindata) != maindata.dircache.size) + ffilesize(fd) - sizeof (maindata) != maindata.dircache.size) { logf("dircache: file header error"); goto error_nolock; diff --git a/firmware/common/file.c b/firmware/common/file.c index 12505381fc..1139385c42 100644 --- a/firmware/common/file.c +++ b/firmware/common/file.c @@ -507,7 +507,7 @@ static int open_internal_inner1(const char *path, int oflag, file_error: if (fildes >= 0) - close(fildes); + rbfs_close(fildes); return rc; } @@ -817,21 +817,21 @@ void force_close_writer_internal(struct filestr_base *stream) /** POSIX **/ /* open a file */ -int open(const char *path, int oflag) +int rbfs_open(const char *path, int oflag) { DEBUGF("open(path=\"%s\",oflag=%X)\n", path, (unsigned)oflag); return open_internal_locked(path, oflag, FF_ANYTYPE); } /* create a new file or rewrite an existing one */ -int creat(const char *path) +int rbfs_creat(const char *path) { DEBUGF("creat(path=\"%s\")\n", path); return open_internal_locked(path, O_WRONLY|O_CREAT|O_TRUNC, FF_ANYTYPE); } /* close a file descriptor */ -int close(int fildes) +int rbfs_close(int fildes) { DEBUGF("close(fd=%d)\n", fildes); @@ -857,7 +857,7 @@ file_error: } /* truncate a file to a specified length */ -int ftruncate(int fildes, off_t length) +int rbfs_ftruncate(int fildes, off_t length) { DEBUGF("ftruncate(fd=%d,len=%ld)\n", fildes, (long)length); @@ -889,7 +889,7 @@ file_error: } /* synchronize changes to a file */ -int fsync(int fildes) +int rbfs_fsync(int fildes) { DEBUGF("fsync(fd=%d)\n", fildes); @@ -915,7 +915,7 @@ file_error: } /* move the read/write file offset */ -off_t lseek(int fildes, off_t offset, int whence) +off_t rbfs_lseek(int fildes, off_t offset, int whence) { #ifndef LOGF_ENABLE /* wipes out log before you can save it */ DEBUGF("lseek(fd=%d,ofs=%ld,wh=%d)\n", fildes, (long)offset, whence); @@ -934,7 +934,7 @@ file_error: } /* read from a file */ -ssize_t read(int fildes, void *buf, size_t nbyte) +ssize_t rbfs_read(int fildes, void *buf, size_t nbyte) { struct filestr_desc * const file = GET_FILESTR(READER, fildes); if (!file) @@ -960,7 +960,7 @@ file_error: } /* write on a file */ -ssize_t write(int fildes, const void *buf, size_t nbyte) +ssize_t rbfs_write(int fildes, const void *buf, size_t nbyte) { struct filestr_desc * const file = GET_FILESTR(READER, fildes); if (!file) @@ -986,7 +986,7 @@ file_error: } /* remove a file */ -int remove(const char *path) +int rbfs_remove(const char *path) { DEBUGF("remove(path=\"%s\")\n", path); @@ -997,7 +997,7 @@ int remove(const char *path) } /* rename a file */ -int rename(const char *old, const char *new) +int rbfs_rename(const char *old, const char *new) { DEBUGF("rename(old=\"%s\",new=\"%s\")\n", old, new); @@ -1144,7 +1144,7 @@ file_error: /** Extensions **/ -int modtime(const char *path, time_t modtime) +int rbfs_modtime(const char *path, time_t modtime) { DEBUGF("modtime(path=\"%s\",modtime=%d)\n", path, (int) modtime); @@ -1181,7 +1181,7 @@ file_error: } /* get the binary size of a file (in bytes) */ -off_t filesize(int fildes) +off_t rbfs_ffilesize(int fildes) { struct filestr_desc * const file = GET_FILESTR(READER, fildes); if (!file) @@ -1200,7 +1200,7 @@ file_error: } /* test if two file descriptors refer to the same file */ -int fsamefile(int fildes1, int fildes2) +int rbfs_fsamefile(int fildes1, int fildes2) { struct filestr_desc * const file1 = GET_FILESTR(WRITER, fildes1); if (!file1) @@ -1217,7 +1217,7 @@ int fsamefile(int fildes1, int fildes2) } /* tell the relationship of path1 to path2 */ -int relate(const char *path1, const char *path2) +int rbfs_relate(const char *path1, const char *path2) { /* this is basically what rename() does but reduced to the relationship determination */ @@ -1277,7 +1277,7 @@ file_error: } /* test file or directory existence */ -bool file_exists(const char *path) +bool rbfs_file_exists(const char *path) { file_internal_lock_WRITER(); bool rc = test_stream_exists_internal(path, FF_ANYTYPE) > 0; diff --git a/firmware/common/rb-loader.c b/firmware/common/rb-loader.c index 2f5e06e165..b8d3d5a163 100644 --- a/firmware/common/rb-loader.c +++ b/firmware/common/rb-loader.c @@ -53,7 +53,7 @@ static int load_firmware_filename(unsigned char* buf, if (fd < 0) return EFILE_NOT_FOUND; - len = filesize(fd) - 8; + len = ffilesize(fd) - 8; if (len > buffer_size) { diff --git a/firmware/common/unicode.c b/firmware/common/unicode.c index 7e5bac8d91..15abc8fde2 100644 --- a/firmware/common/unicode.c +++ b/firmware/common/unicode.c @@ -221,7 +221,7 @@ static int alloc_and_load_cp_table(int cp, void *buf) if (fd < 0) return -1; - off_t size = filesize(fd); + off_t size = ffilesize(fd); if (size > 0 && size <= MAX_CP_TABLE_SIZE*2 && !(size % (off_t)sizeof (uint16_t))) { diff --git a/firmware/common/zip.c b/firmware/common/zip.c index 345eb6cb35..3edeceae82 100644 --- a/firmware/common/zip.c +++ b/firmware/common/zip.c @@ -524,7 +524,7 @@ static off_t zip_file_seek(struct zip* zh, off_t offset, int whence) { static off_t zip_file_size(struct zip* zh) { struct zip_file* z = (struct zip_file*) zh; - return filesize(z->file); + return ffilesize(z->file); } static void zip_file_close(struct zip* zh) { @@ -721,7 +721,7 @@ struct zip* zip_open(const char* name, bool try_mem) { if ((file = open(name, O_RDONLY)) < 0) goto bail; - if (try_mem && (mem_handle = zip_core_alloc(mem_size = filesize(file))) >= 0) { + if (try_mem && (mem_handle = zip_core_alloc(mem_size = ffilesize(file))) >= 0) { if ((zip_handle = zip_core_alloc(sizeof(struct zip_mem))) < 0) goto bail; diff --git a/firmware/font.c b/firmware/font.c index 9ea194038a..f3d14c48f0 100644 --- a/firmware/font.c +++ b/firmware/font.c @@ -435,7 +435,7 @@ int font_load_ex( const char *path, size_t buf_size, int glyphs ) #endif /* load font struct f with file header */ - int file_size = filesize( fd ); + int file_size = ffilesize( fd ); struct font header; struct font f; @@ -1021,7 +1021,7 @@ static NO_INLINE void glyph_cache_load(const char *font_path, struct font *pf) if (hdr != FC_HEADER_VAL) goto latin; /* only read what fits */ - glyph_file_size = filesize( fd ); + glyph_file_size = ffilesize( fd ); if (glyph_file_size < sizeof(uint32_t)) goto latin; glyph_file_size -= sizeof(uint32_t); diff --git a/firmware/include/dir.h b/firmware/include/dir.h index 844b56ac1d..1619242cc3 100644 --- a/firmware/include/dir.h +++ b/firmware/include/dir.h @@ -65,6 +65,9 @@ #ifndef dir_exists #define dir_exists FS_PREFIX(dir_exists) #endif +#ifndef dir_get_info +#define dir_get_info FS_PREFIX(dir_get_info) +#endif #ifndef root_realpath #define root_realpath FS_PREFIX(root_realpath) #endif @@ -85,12 +88,4 @@ struct dirinfo time_t mtime; /* local file time */ }; -#ifndef DIRFUNCTIONS_DECLARED -/* TIP: set errno to zero before calling to see if anything failed */ -struct dirinfo dir_get_info(DIR *dirp, struct DIRENT *entry); -const char* root_realpath(void); -#endif /* !DIRFUNCTIONS_DECLARED */ - - - #endif /* _DIR_H_ */ diff --git a/firmware/include/dircache_redirect.h b/firmware/include/dircache_redirect.h index d7af8d78d9..329df8b3a6 100644 --- a/firmware/include/dircache_redirect.h +++ b/firmware/include/dircache_redirect.h @@ -192,7 +192,7 @@ static inline void volume_onmount_internal(IF_MV_NONVOID(int volume)) if (rtlen <= 0 || rtlen >= (int) sizeof(rtpath)) rtlen = 0; /* path too long or sprintf error */ - else if (file_exists(rtpath)) + else if (rbfs_file_exists(rtpath)) { rtlen = get_redirect_dir(rtpath, sizeof(rtpath), volume, "", ""); while (rtlen > 0 && rtpath[--rtlen] == PATH_SEPCH) diff --git a/firmware/include/file.h b/firmware/include/file.h index 470550a2a7..6a1b3795d0 100644 --- a/firmware/include/file.h +++ b/firmware/include/file.h @@ -87,8 +87,8 @@ int fdprintf(int fildes, const char *fmt, ...) ATTRIBUTE_PRINTF(2, 3); #ifndef modtime #define modtime FS_PREFIX(modtime) #endif -#ifndef filesize -#define filesize FS_PREFIX(filesize) +#ifndef ffilesize +#define ffilesize FS_PREFIX(ffilesize) #endif #ifndef fsamefile #define fsamefile FS_PREFIX(fsamefile) diff --git a/firmware/include/filesystem-native.h b/firmware/include/filesystem-native.h index 0edce81ae3..aaced66429 100644 --- a/firmware/include/filesystem-native.h +++ b/firmware/include/filesystem-native.h @@ -27,7 +27,7 @@ #define DIRFUNCTIONS_DEFINED #endif /* PLUGIN || CODEC */ -#define FS_PREFIX(_x_) _x_ +#define FS_PREFIX(_x_) rbfs_ ## _x_ #endif /* _FILESYSTEM_NATIVE_H_ */ #ifdef _FILE_H_ @@ -44,26 +44,26 @@ #include -int open(const char *name, int oflag); -int creat(const char *name); -int close(int fildes); -int ftruncate(int fildes, off_t length); -int fsync(int fildes); -off_t lseek(int fildes, off_t offset, int whence); -ssize_t read(int fildes, void *buf, size_t nbyte); -ssize_t write(int fildes, const void *buf, size_t nbyte); -int remove(const char *path); -int rename(const char *old, const char *new); -int modtime(const char *path, time_t modtime); -off_t filesize(int fildes); -int fsamefile(int fildes1, int fildes2); -int relate(const char *path1, const char *path2); -bool file_exists(const char *path); +int rbfs_open(const char *name, int oflag); +int rbfs_creat(const char *name); +int rbfs_close(int fildes); +int rbfs_ftruncate(int fildes, off_t length); +int rbfs_fsync(int fildes); +off_t rbfs_lseek(int fildes, off_t offset, int whence); +ssize_t rbfs_read(int fildes, void *buf, size_t nbyte); +ssize_t rbfs_write(int fildes, const void *buf, size_t nbyte); +int rbfs_remove(const char *path); +int rbfs_rename(const char *old, const char *new); +int rbfs_modtime(const char *path, time_t modtime); +off_t rbfs_ffilesize(int fildes); +int rbfs_fsamefile(int fildes1, int fildes2); +int rbfs_relate(const char *path1, const char *path2); +bool rbfs_file_exists(const char *path); #endif /* !FILEFUNCTIONS_DECLARED */ #if !defined(RB_FILESYSTEM_OS) && !defined (FILEFUNCTIONS_DEFINED) -#define open(path, oflag, ...) open(path, oflag) -#define creat(path, mode) creat(path) +#define rbfs_open(path, oflag, ...) rbfs_open(path, oflag) +#define rbfs_creat(path, mode) rbfs_creat(path) #endif /* FILEFUNCTIONS_DEFINED */ #endif /* _FILESYSTEM_NATIVE__FILE_H_ */ @@ -93,16 +93,19 @@ typedef struct {} DIR; #define DIRFUNCTIONS_DEFINED #endif -DIR * opendir(const char *dirname); -struct dirent * readdir(DIR *dirp); -int readdir_r(DIR *dirp, struct dirent *entry, - struct dirent **result); -void rewinddir(DIR *dirp); -int closedir(DIR *dirp); -int mkdir(const char *path); -int rmdir(const char *path); -int samedir(DIR *dirp1, DIR *dirp2); -bool dir_exists(const char *dirname); +DIR * rbfs_opendir(const char *dirname); +struct dirent * rbfs_readdir(DIR *dirp); +int rbfs_readdir_r(DIR *dirp, struct dirent *entry, + struct dirent **result); +void rbfs_rewinddir(DIR *dirp); +int rbfs_closedir(DIR *dirp); +int rbfs_mkdir(const char *path); +int rbfs_rmdir(const char *path); +int rbfs_samedir(DIR *dirp1, DIR *dirp2); +bool rbfs_dir_exists(const char *dirname); + +struct dirinfo rbfs_dir_get_info(DIR *dirp, struct DIRENT *entry); +const char * rbfs_root_realpath(void); #endif /* !DIRFUNCTIONS_DECLARED */ #endif /* _FILESYSTEM_NATIVE__DIR_H_ */ diff --git a/firmware/lc-rock.c b/firmware/lc-rock.c index 73d743dfbd..23adb7f390 100644 --- a/firmware/lc-rock.c +++ b/firmware/lc-rock.c @@ -60,7 +60,7 @@ void * lc_open(const char *filename, unsigned char *buf, size_t buf_size) /* hdr.end_addr points to the end of the bss section, * but there might be idata/icode behind that so the bytes to copy * can be larger */ - copy_size = MAX(filesize(fd), hdr.end_addr - hdr.load_addr); + copy_size = MAX(ffilesize(fd), hdr.end_addr - hdr.load_addr); if (hdr.load_addr < buf || (hdr.load_addr+copy_size) > buf_end) { diff --git a/firmware/target/hosted/filesystem-app.c b/firmware/target/hosted/filesystem-app.c index 03a7bf7f18..4b0c60d962 100644 --- a/firmware/target/hosted/filesystem-app.c +++ b/firmware/target/hosted/filesystem-app.c @@ -504,7 +504,7 @@ bool app_dir_exists(const char *dirname) return true; } -struct dirinfo dir_get_info(DIR *dirp, struct dirent *entry) +struct dirinfo app_dir_get_info(DIR *dirp, struct dirent *entry) { struct __dir *this = (struct __dir *)dirp; struct dirinfo ret = { .attribute = 0, diff --git a/firmware/target/hosted/filesystem-app.h b/firmware/target/hosted/filesystem-app.h index c526a1bdcf..d03dadeb01 100644 --- a/firmware/target/hosted/filesystem-app.h +++ b/firmware/target/hosted/filesystem-app.h @@ -86,7 +86,7 @@ ssize_t app_write(int fildes, const void *buf, size_t nbyte); int app_remove(const char *path); int app_rename(const char *old, const char *new); #define app_modtime os_modtime -#define app_filesize os_filesize +#define app_ffilesize os_ffilesize #define app_fsamefile os_fsamefile int app_relate(const char *path1, const char *path2); bool app_file_exists(const char *path); @@ -115,6 +115,8 @@ int app_mkdir(const char *path); int app_rmdir(const char *path); int app_samedir(DIR *dirp1, DIR *dirp2); bool app_dir_exists(const char *dirname); +struct dirinfo app_dir_get_info(DIR *dirp, struct dirent *entry); +const char * app_root_realpath(void); #endif /* DIRFUNCTIONS_DECLARED */ #endif /* _FILESYSTEM_APP__DIR_H_ */ diff --git a/firmware/target/hosted/filesystem-console.h b/firmware/target/hosted/filesystem-console.h index ee10a725e5..9ad880f259 100644 --- a/firmware/target/hosted/filesystem-console.h +++ b/firmware/target/hosted/filesystem-console.h @@ -63,7 +63,7 @@ ssize_t console_write(int fildes, const void *buf, size_t nbyte); int console_remove(const char *path); int console_rename(const char *old, const char *new); int console_modtime(const char *path, time_t modtime); -off_t console_filesize(int fildes); +off_t console_ffilesize(int fildes); int console_fsamefile(int fildes1, int fildes2); int console_relate(const char *path1, const char *path2); bool console_file_exists(const char *path); @@ -109,6 +109,9 @@ int console_mkdir(const char *path); int console_rmdir(const char *path); int console_samedir(DIR *dirp1, DIR *dirp2); bool console_dir_exists(const char *dirname); + +struct dirinfo console_dir_get_info(DIR *dirp, struct dirent *entry); +const char* console_root_realpath(void); #endif /* !DIRFUNCTIONS_DECLARED */ #endif /* _FILESYSTEM_CONSOLE__DIR_H_ */ diff --git a/firmware/target/hosted/filesystem-hosted.h b/firmware/target/hosted/filesystem-hosted.h index 3716da76f7..38e0b73067 100644 --- a/firmware/target/hosted/filesystem-hosted.h +++ b/firmware/target/hosted/filesystem-hosted.h @@ -30,7 +30,7 @@ void * os_lc_open(const char *ospath); #ifndef OSFUNCTIONS_DECLARED int os_modtime(const char *path, time_t modtime); -off_t os_filesize(int osfd); +off_t os_ffilesize(int osfd); int os_fsamefile(int osfd1, int osfd2); int os_relate(const char *path1, const char *path2); bool os_file_exists(const char *ospath); diff --git a/firmware/target/hosted/filesystem-unix.c b/firmware/target/hosted/filesystem-unix.c index 5e4c2a7207..068c49cd6f 100644 --- a/firmware/target/hosted/filesystem-unix.c +++ b/firmware/target/hosted/filesystem-unix.c @@ -53,7 +53,7 @@ int os_modtime(const char *path, time_t modtime) return utime(path, ×); } -off_t os_filesize(int osfd) +off_t os_ffilesize(int osfd) { struct stat sb; diff --git a/firmware/target/hosted/filesystem-win32.h b/firmware/target/hosted/filesystem-win32.h index 68f77dbf98..2627d46b48 100644 --- a/firmware/target/hosted/filesystem-win32.h +++ b/firmware/target/hosted/filesystem-win32.h @@ -49,12 +49,12 @@ size_t strlcpy_utf16utf8(char *buffer, const unsigned short *utf16, #ifndef OSFUNCTIONS_DECLARED /* Wrap for off_t <=> long conversions */ -static inline off_t os_filesize_(int osfd) +static inline off_t os_ffilesize_(int osfd) { return _filelength(osfd); } static inline int os_ftruncate_(int osfd, off_t length) { return _chsize(osfd, length); } -#define os_filesize os_filesize_ +#define os_ffilesize os_ffilesize_ #define os_ftruncate os_ftruncate_ #define os_fsync _commit #define os_fstat _fstat diff --git a/firmware/target/hosted/game_console/lib/sys_dir_3ds.c b/firmware/target/hosted/game_console/lib/sys_dir_3ds.c index 91a441987f..b83d727b4b 100644 --- a/firmware/target/hosted/game_console/lib/sys_dir_3ds.c +++ b/firmware/target/hosted/game_console/lib/sys_dir_3ds.c @@ -390,7 +390,7 @@ file_error: } /* get the portable info from the native entry */ -struct dirinfo dir_get_info(DIR *dirp, struct dirent *entry) +struct dirinfo console_dir_get_info(DIR *dirp, struct dirent *entry) { int rc; if (!dirp || !entry) diff --git a/firmware/target/hosted/game_console/lib/sys_file_common.c b/firmware/target/hosted/game_console/lib/sys_file_common.c index 2eab3dd4a3..481d0fc9a5 100644 --- a/firmware/target/hosted/game_console/lib/sys_file_common.c +++ b/firmware/target/hosted/game_console/lib/sys_file_common.c @@ -325,7 +325,7 @@ file_error: /** Extensions **/ /* get the binary size of a file (in bytes) */ -off_t console_filesize(int fildes) +off_t console_ffilesize(int fildes) { struct filestr_desc * const file = GET_FILESTR(READER, fildes); if (!file) @@ -381,4 +381,4 @@ bool console_file_exists(const char *path) bool rc = test_stream_exists_internal(path) > 0; file_internal_unlock_WRITER(); return rc; -} \ No newline at end of file +} diff --git a/lib/rbcodec/codecs/librm/rm.c b/lib/rbcodec/codecs/librm/rm.c index 49ec6da0a3..e7d9bcab71 100644 --- a/lib/rbcodec/codecs/librm/rm.c +++ b/lib/rbcodec/codecs/librm/rm.c @@ -32,7 +32,7 @@ #include "inttypes.h" #include -int filesize(int fd) +int ffilesize(int fd) { struct stat buf; diff --git a/lib/rbcodec/codecs/librm/rm.h b/lib/rbcodec/codecs/librm/rm.h index 8e4d124d68..1e110a956f 100644 --- a/lib/rbcodec/codecs/librm/rm.h +++ b/lib/rbcodec/codecs/librm/rm.h @@ -95,7 +95,7 @@ int rm_get_packet(uint8_t **src,RMContext *rmctx, RMPacket *pkt); #ifdef TEST -int filesize(int fd); +int ffilesize(int fd); void advance_buffer(uint8_t **buf, int val); /* Get a (sub_packet_h*frames_per_packet) number of audio frames from a file descriptor */ diff --git a/lib/rbcodec/metadata/a52.c b/lib/rbcodec/metadata/a52.c index 2176b17a4f..9f00b8d9f3 100644 --- a/lib/rbcodec/metadata/a52.c +++ b/lib/rbcodec/metadata/a52.c @@ -70,7 +70,7 @@ bool get_a52_metadata(int fd, struct mp3entry *id3) id3->bitrate = a52_bitrates[i >> 1]; id3->vbr = false; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); switch (buf[4] & 0xc0) { diff --git a/lib/rbcodec/metadata/aac.c b/lib/rbcodec/metadata/aac.c index e7312829bd..23a430b5b6 100644 --- a/lib/rbcodec/metadata/aac.c +++ b/lib/rbcodec/metadata/aac.c @@ -72,7 +72,7 @@ bool get_aac_metadata(int fd, struct mp3entry *entry) entry->id3v1len = 0; entry->id3v2len = getid3v2len(fd); entry->first_frame_offset = entry->id3v2len; - entry->filesize = filesize(fd) - entry->first_frame_offset; + entry->filesize = ffilesize(fd) - entry->first_frame_offset; entry->needs_upsampling_correction = false; if (entry->id3v2len) diff --git a/lib/rbcodec/metadata/adx.c b/lib/rbcodec/metadata/adx.c index ce7429a259..6f9c642539 100644 --- a/lib/rbcodec/metadata/adx.c +++ b/lib/rbcodec/metadata/adx.c @@ -69,7 +69,7 @@ bool get_adx_metadata(int fd, struct mp3entry* id3) id3->bitrate = id3->frequency * channels * 18 * 8 / 32 / 1000; id3->length = get_long_be(&buf[12]) / id3->frequency * 1000; id3->vbr = false; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); /* get loop info */ if (!memcmp(buf+0x10,"\x01\xF4\x03",3)) { diff --git a/lib/rbcodec/metadata/aiff.c b/lib/rbcodec/metadata/aiff.c index 9b5f772891..2ac0fe3f02 100644 --- a/lib/rbcodec/metadata/aiff.c +++ b/lib/rbcodec/metadata/aiff.c @@ -142,7 +142,7 @@ bool get_aiff_metadata(int fd, struct mp3entry* id3) } id3->vbr = false; /* AIFF files are CBR */ - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); } else { diff --git a/lib/rbcodec/metadata/asap.c b/lib/rbcodec/metadata/asap.c index 7524d2c6f3..a5a207e8d9 100644 --- a/lib/rbcodec/metadata/asap.c +++ b/lib/rbcodec/metadata/asap.c @@ -241,7 +241,7 @@ static bool parse_sap_header(int fd, struct mp3entry* id3, int file_len) bool get_asap_metadata(int fd, struct mp3entry* id3) { - int filelength = filesize(fd); + int filelength = ffilesize(fd); if(parse_sap_header(fd, id3, filelength) == false) { diff --git a/lib/rbcodec/metadata/asf.c b/lib/rbcodec/metadata/asf.c index cf260ad752..6dadda2247 100644 --- a/lib/rbcodec/metadata/asf.c +++ b/lib/rbcodec/metadata/asf.c @@ -647,7 +647,7 @@ bool get_asf_metadata(int fd, struct mp3entry* id3) header. */ id3->first_frame_offset = lseek(fd, 0, SEEK_CUR) + 26; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); /* We copy the wfx struct to the MP3 TOC field in the id3 struct so the codec doesn't need to parse the header object again */ memcpy(id3->toc, &wfx, sizeof(wfx)); diff --git a/lib/rbcodec/metadata/au.c b/lib/rbcodec/metadata/au.c index 7cdd975c7e..52b24823ca 100644 --- a/lib/rbcodec/metadata/au.c +++ b/lib/rbcodec/metadata/au.c @@ -58,7 +58,7 @@ bool get_au_metadata(int fd, struct mp3entry* id3) int offset; id3->vbr = false; /* All Sun audio files are CBR */ - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); id3->length = 0; lseek(fd, 0, SEEK_SET); diff --git a/lib/rbcodec/metadata/ay.c b/lib/rbcodec/metadata/ay.c index 4230b44da0..d4c6cc8880 100644 --- a/lib/rbcodec/metadata/ay.c +++ b/lib/rbcodec/metadata/ay.c @@ -135,7 +135,7 @@ bool get_ay_metadata(int fd, struct mp3entry* id3) return false; id3->vbr = false; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); id3->bitrate = 706; id3->frequency = 44100; diff --git a/lib/rbcodec/metadata/flac.c b/lib/rbcodec/metadata/flac.c index 6ff69f65a5..c5d26f08e3 100644 --- a/lib/rbcodec/metadata/flac.c +++ b/lib/rbcodec/metadata/flac.c @@ -121,7 +121,7 @@ bool get_flac_metadata(int fd, struct mp3entry* id3) } id3->vbr = true; /* All FLAC files are VBR */ - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); id3->frequency = (buf[10] << 12) | (buf[11] << 4) | ((buf[12] & 0xf0) >> 4); rc = true; /* Got vital metadata */ diff --git a/lib/rbcodec/metadata/gbs.c b/lib/rbcodec/metadata/gbs.c index 9b02952e47..e83395dc34 100644 --- a/lib/rbcodec/metadata/gbs.c +++ b/lib/rbcodec/metadata/gbs.c @@ -52,7 +52,7 @@ bool get_gbs_metadata(int fd, struct mp3entry* id3) return false; id3->vbr = false; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); /* we only render 16 bits, 44.1KHz, Stereo */ id3->bitrate = 706; id3->frequency = 44100; diff --git a/lib/rbcodec/metadata/hes.c b/lib/rbcodec/metadata/hes.c index 87398461d7..f9eab6a0e7 100644 --- a/lib/rbcodec/metadata/hes.c +++ b/lib/rbcodec/metadata/hes.c @@ -26,7 +26,7 @@ bool get_hes_metadata(int fd, struct mp3entry* id3) return false; id3->vbr = false; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); /* we only render 16 bits, 44.1KHz, Stereo */ id3->bitrate = 706; id3->frequency = 44100; diff --git a/lib/rbcodec/metadata/kss.c b/lib/rbcodec/metadata/kss.c index 195e8be38d..bd85a02a4f 100644 --- a/lib/rbcodec/metadata/kss.c +++ b/lib/rbcodec/metadata/kss.c @@ -40,7 +40,7 @@ bool get_kss_metadata(int fd, struct mp3entry* id3) return false; id3->vbr = false; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); /* we only render 16 bits, 44.1KHz, Stereo */ id3->bitrate = 706; id3->frequency = 44100; diff --git a/lib/rbcodec/metadata/metadata.c b/lib/rbcodec/metadata/metadata.c index dd41fdbaa7..e2ca3662c5 100644 --- a/lib/rbcodec/metadata/metadata.c +++ b/lib/rbcodec/metadata/metadata.c @@ -41,7 +41,7 @@ static bool get_shn_metadata(int fd, struct mp3entry *id3) { /* TODO: read the id3v2 header if it exists */ id3->vbr = true; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); return skip_id3v2(fd, id3); } @@ -50,7 +50,7 @@ static bool get_other_asap_metadata(int fd, struct mp3entry *id3) id3->bitrate = 706; id3->frequency = 44100; id3->vbr = false; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); id3->genre_string = id3_get_num_genre(36); /* GAME */ return true; } diff --git a/lib/rbcodec/metadata/mod.c b/lib/rbcodec/metadata/mod.c index 746a4f6022..30f6658a2f 100644 --- a/lib/rbcodec/metadata/mod.c +++ b/lib/rbcodec/metadata/mod.c @@ -91,11 +91,11 @@ bool get_mod_metadata(int fd, struct mp3entry* id3) return false; id3->title = id3->id3v2buf; /* Point title to previous read ID3 buffer. */ - id3->bitrate = filesize(fd)/1024; /* size in kb */ id3->frequency = 44100; id3->length = 120*1000; id3->vbr = false; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); + id3->bitrate = id3->filesize / 1024; /* size in kb */ return true; } diff --git a/lib/rbcodec/metadata/monkeys.c b/lib/rbcodec/metadata/monkeys.c index 382aa65498..7ea4426b46 100644 --- a/lib/rbcodec/metadata/monkeys.c +++ b/lib/rbcodec/metadata/monkeys.c @@ -82,7 +82,7 @@ bool get_monkeys_metadata(int fd, struct mp3entry* id3) } id3->vbr = true; /* All APE files are VBR */ - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); totalsamples = finalframeblocks; if (totalframes > 1) diff --git a/lib/rbcodec/metadata/mp3.c b/lib/rbcodec/metadata/mp3.c index b2466f82a0..733dfdbee2 100644 --- a/lib/rbcodec/metadata/mp3.c +++ b/lib/rbcodec/metadata/mp3.c @@ -162,7 +162,7 @@ static int getsonglength(int fd, struct mp3entry *entry) bool get_mp3_metadata(int fd, struct mp3entry *entry) { entry->title = NULL; - entry->filesize = filesize(fd); + entry->filesize = ffilesize(fd); entry->id3v1len = getid3v1len(fd); entry->id3v2len = getid3v2len(fd); diff --git a/lib/rbcodec/metadata/mp3data.c b/lib/rbcodec/metadata/mp3data.c index 5c2db376e5..45424d92e5 100644 --- a/lib/rbcodec/metadata/mp3data.c +++ b/lib/rbcodec/metadata/mp3data.c @@ -608,7 +608,7 @@ int get_mp3file_info(int fd, struct mp3info *info) if(result) return result; - info->byte_count = filesize(fd) - getid3v1len(fd) - offset - bytecount; + info->byte_count = ffilesize(fd) - getid3v1len(fd) - offset - bytecount; } return bytecount; diff --git a/lib/rbcodec/metadata/mp4.c b/lib/rbcodec/metadata/mp4.c index f02d301077..9d6e9e4334 100644 --- a/lib/rbcodec/metadata/mp4.c +++ b/lib/rbcodec/metadata/mp4.c @@ -823,7 +823,7 @@ bool get_mp4_metadata(int fd, struct mp3entry* id3) id3->filesize = 0; errno = 0; - if (read_mp4_container(fd, id3, filesize(fd)) && (errno == 0) + if (read_mp4_container(fd, id3, ffilesize(fd)) && (errno == 0) && (id3->samples > 0) && (id3->frequency > 0) && (id3->filesize > 0)) { diff --git a/lib/rbcodec/metadata/mpc.c b/lib/rbcodec/metadata/mpc.c index 326cc471ec..5db0e0041d 100644 --- a/lib/rbcodec/metadata/mpc.c +++ b/lib/rbcodec/metadata/mpc.c @@ -211,7 +211,7 @@ bool get_musepack_metadata(int fd, struct mp3entry *id3) return false; } - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); id3->bitrate = id3->filesize * 8 / id3->length; read_ape_tags(fd, id3); diff --git a/lib/rbcodec/metadata/nsf.c b/lib/rbcodec/metadata/nsf.c index 3184897e22..5a59470163 100644 --- a/lib/rbcodec/metadata/nsf.c +++ b/lib/rbcodec/metadata/nsf.c @@ -262,7 +262,7 @@ bool get_nsf_metadata(int fd, struct mp3entry* id3) return false; id3->vbr = false; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); /* we only render 16 bits, 44.1KHz, Mono */ id3->bitrate = 706; id3->frequency = 44100; diff --git a/lib/rbcodec/metadata/ogg.c b/lib/rbcodec/metadata/ogg.c index b172877351..b6fb3ab482 100644 --- a/lib/rbcodec/metadata/ogg.c +++ b/lib/rbcodec/metadata/ogg.c @@ -133,7 +133,7 @@ bool get_ogg_metadata(int fd, struct mp3entry* id3) return false; } - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); /* We need to ensure the serial number from this page is the same as the * one from the last page (since we only support a single bitstream). diff --git a/lib/rbcodec/metadata/oma.c b/lib/rbcodec/metadata/oma.c index 3afa1aad70..42fc275412 100644 --- a/lib/rbcodec/metadata/oma.c +++ b/lib/rbcodec/metadata/oma.c @@ -179,7 +179,7 @@ bool get_oma_metadata(int fd, struct mp3entry* id3) /* Currently, there's no means of knowing the duration * * directly from the the file so we calculate it. */ - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); id3->length = ((id3->filesize - id3->first_frame_offset) * 8) / id3->bitrate; return true; } diff --git a/lib/rbcodec/metadata/rm.c b/lib/rbcodec/metadata/rm.c index 0bffe6dfdb..25f49c1880 100644 --- a/lib/rbcodec/metadata/rm.c +++ b/lib/rbcodec/metadata/rm.c @@ -266,7 +266,7 @@ static inline int rm_parse_header(int fd, RMContext *rmctx, struct mp3entry *id3 rmctx->data_offset = skipped + 8; rmctx->bit_rate = rmctx->block_align * rmctx->sample_rate / 192; if (rmctx->block_align) - rmctx->nb_packets = (filesize(fd) - rmctx->data_offset) / rmctx->block_align; + rmctx->nb_packets = (ffilesize(fd) - rmctx->data_offset) / rmctx->block_align; if (rmctx->sample_rate) rmctx->duration = (uint32_t)(256LL * 6 * 1000 * rmctx->nb_packets / rmctx->sample_rate); rmctx->flags |= RM_RAW_DATASTREAM; @@ -504,6 +504,6 @@ bool get_rm_metadata(int fd, struct mp3entry* id3) id3->bitrate = (rmctx->bit_rate + 500) / 1000; id3->frequency = rmctx->sample_rate; id3->length = rmctx->duration; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); return true; } diff --git a/lib/rbcodec/metadata/sgc.c b/lib/rbcodec/metadata/sgc.c index 11a574139f..92a7820d10 100644 --- a/lib/rbcodec/metadata/sgc.c +++ b/lib/rbcodec/metadata/sgc.c @@ -54,7 +54,7 @@ bool get_sgc_metadata(int fd, struct mp3entry* id3) return false; id3->vbr = false; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); /* we only render 16 bits, 44.1KHz, Stereo */ id3->bitrate = 706; id3->frequency = 44100; diff --git a/lib/rbcodec/metadata/sid.c b/lib/rbcodec/metadata/sid.c index 43f693e985..94bafad70d 100644 --- a/lib/rbcodec/metadata/sid.c +++ b/lib/rbcodec/metadata/sid.c @@ -82,7 +82,7 @@ bool get_sid_metadata(int fd, struct mp3entry* id3) */ id3->length = (buf[0xf]-1)*1000; id3->vbr = false; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); return true; } diff --git a/lib/rbcodec/metadata/smaf.c b/lib/rbcodec/metadata/smaf.c index 479c39032b..130edb98fa 100644 --- a/lib/rbcodec/metadata/smaf.c +++ b/lib/rbcodec/metadata/smaf.c @@ -445,7 +445,7 @@ bool get_smaf_metadata(int fd, struct mp3entry* id3) id3->composer = NULL; id3->vbr = false; /* All SMAF files are CBR */ - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); /* check File Chunk and Contents Info Chunk */ lseek(fd, 0, SEEK_SET); diff --git a/lib/rbcodec/metadata/spc.c b/lib/rbcodec/metadata/spc.c index 64e00ef830..d2a03fd0e4 100644 --- a/lib/rbcodec/metadata/spc.c +++ b/lib/rbcodec/metadata/spc.c @@ -124,7 +124,7 @@ bool get_spc_metadata(int fd, struct mp3entry* id3) id3->frequency = 32000; id3->length = length+fade; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); id3->genre_string = id3_get_num_genre(36); return true; diff --git a/lib/rbcodec/metadata/tta.c b/lib/rbcodec/metadata/tta.c index ef0f341f29..4d2cfa84a7 100644 --- a/lib/rbcodec/metadata/tta.c +++ b/lib/rbcodec/metadata/tta.c @@ -53,7 +53,7 @@ static void read_id3_tags(int fd, struct mp3entry* id3) { id3->title = NULL; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); id3->id3v2len = getid3v2len(fd); id3->vbr = false; /* All TTA files are CBR */ diff --git a/lib/rbcodec/metadata/vgm.c b/lib/rbcodec/metadata/vgm.c index 03afb15a92..28a2a07b8a 100644 --- a/lib/rbcodec/metadata/vgm.c +++ b/lib/rbcodec/metadata/vgm.c @@ -153,7 +153,7 @@ bool get_vgm_metadata(int fd, struct mp3entry* id3) } id3->vbr = false; - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); id3->bitrate = 1411; id3->frequency = 44100; diff --git a/lib/rbcodec/metadata/vox.c b/lib/rbcodec/metadata/vox.c index dcf4e057ef..22a24101c9 100644 --- a/lib/rbcodec/metadata/vox.c +++ b/lib/rbcodec/metadata/vox.c @@ -41,7 +41,7 @@ bool get_vox_metadata(int fd, struct mp3entry* id3) id3->frequency = 8000; id3->bitrate = 8000 * 4 / 1000; id3->vbr = false; /* All VOX files are CBR */ - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); id3->length = id3->filesize >> 2; return true; diff --git a/lib/rbcodec/metadata/vtx.c b/lib/rbcodec/metadata/vtx.c index 65401be8b8..e27ae170c9 100644 --- a/lib/rbcodec/metadata/vtx.c +++ b/lib/rbcodec/metadata/vtx.c @@ -102,7 +102,7 @@ bool get_vtx_metadata(int fd, struct mp3entry* id3) if (lseek(fd, 0, SEEK_SET) < 0) goto exit_bad; - if (filesize(fd) < 20) + if (ffilesize(fd) < 20) goto exit_bad; uint hdr = Reader_ReadWord(fd); @@ -142,7 +142,7 @@ bool get_vtx_metadata(int fd, struct mp3entry* id3) id3->bitrate = 706; id3->frequency = 44100; // XXX allow this to be configured? - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); id3->length = info.frames * 1000 / info.playerfreq; return true; diff --git a/lib/rbcodec/metadata/wave.c b/lib/rbcodec/metadata/wave.c index 9ecc816832..7d0b9e8e9b 100644 --- a/lib/rbcodec/metadata/wave.c +++ b/lib/rbcodec/metadata/wave.c @@ -305,7 +305,7 @@ static bool read_header(int fd, struct mp3entry* id3, const unsigned char *chunk memset(&fmt, 0, sizeof(struct wave_fmt)); id3->vbr = false; /* All Wave/Wave64 files are CBR */ - id3->filesize = filesize(fd); + id3->filesize = ffilesize(fd); /* get RIFF chunk header */ lseek(fd, 0, SEEK_SET); diff --git a/lib/rbcodec/metadata/wavpack.c b/lib/rbcodec/metadata/wavpack.c index 30be661216..18014f00e5 100644 --- a/lib/rbcodec/metadata/wavpack.c +++ b/lib/rbcodec/metadata/wavpack.c @@ -78,7 +78,7 @@ bool get_wavpack_metadata(int fd, struct mp3entry* id3) } id3->vbr = true; /* All WavPack files are VBR */ - id3->filesize = filesize (fd); + id3->filesize = ffilesize (fd); /* check up to 16 headers before we give up finding one with audio */ diff --git a/lib/rbcodec/rbcodecplatform-unix.h b/lib/rbcodec/rbcodecplatform-unix.h index 9ce01a0035..b25aee24a3 100644 --- a/lib/rbcodec/rbcodecplatform-unix.h +++ b/lib/rbcodec/rbcodecplatform-unix.h @@ -50,7 +50,7 @@ #endif /* filesize */ -off_t filesize(int fd); +off_t ffilesize(int fd); /* snprintf */ #include diff --git a/lib/rbcodec/test/file.h b/lib/rbcodec/test/file.h index ddf9a1d7a4..e37c986df9 100644 --- a/lib/rbcodec/test/file.h +++ b/lib/rbcodec/test/file.h @@ -3,4 +3,4 @@ #include #include -off_t filesize(int fd); +off_t ffilesize(int fd); diff --git a/lib/rbcodec/test/warble.c b/lib/rbcodec/test/warble.c index 75591b99dc..10bc7485a9 100644 --- a/lib/rbcodec/test/warble.c +++ b/lib/rbcodec/test/warble.c @@ -88,7 +88,7 @@ int find_first_set_bit(uint32_t value) return __builtin_ctz(value); } -off_t filesize(int fd) +off_t ffilesize(int fd) { struct stat st; fstat(fd, &st); @@ -844,7 +844,7 @@ static void decode_file(const char *input_fn) exit(1); } print_mp3entry(&id3, stderr); - ci.filesize = filesize(input_fd); + ci.filesize = ffilesize(input_fd); ci.id3 = &id3; if (use_dsp) { ci.dsp = dsp_get_config(CODEC_IDX_AUDIO); diff --git a/uisimulator/common/filesystem-sim.c b/uisimulator/common/filesystem-sim.c index 16e852901b..aa7d394346 100644 --- a/uisimulator/common/filesystem-sim.c +++ b/uisimulator/common/filesystem-sim.c @@ -467,7 +467,7 @@ int sim_ftruncate(int fildes, off_t length) if (!filestr) return -1; - off_t size = os_filesize(filestr->osfd); + off_t size = os_ffilesize(filestr->osfd); if (size < 0) return -1; @@ -582,13 +582,13 @@ int sim_modtime(const char *path, time_t modtime) return os_modtime(ospath, modtime); } -off_t sim_filesize(int fildes) +off_t sim_ffilesize(int fildes) { struct filestr_desc *filestr = get_filestr(fildes); if (!filestr) return -1; - return os_filesize(filestr->osfd); + return os_ffilesize(filestr->osfd); } int sim_fsamefile(int fildes1, int fildes2) @@ -764,7 +764,7 @@ bool sim_dir_exists(const char *dirname) return true; } -struct dirinfo dir_get_info(DIR *dirp, struct sim_dirent *entry) +struct dirinfo sim_dir_get_info(DIR *dirp, struct sim_dirent *entry) { int rc; struct dirstr_desc *dirstr = get_dirstr(dirp); diff --git a/uisimulator/common/filesystem-sim.h b/uisimulator/common/filesystem-sim.h index 38fff3bd4e..8e50709f85 100644 --- a/uisimulator/common/filesystem-sim.h +++ b/uisimulator/common/filesystem-sim.h @@ -68,7 +68,7 @@ ssize_t sim_write(int fildes, const void *buf, size_t nbyte); int sim_remove(const char *path); int sim_rename(const char *old, const char *new); int sim_modtime(const char *path, time_t modtime); -off_t sim_filesize(int fildes); +off_t sim_ffilesize(int fildes); int sim_fsamefile(int fildes1, int fildes2); int sim_relate(const char *path1, const char *path2); bool sim_file_exists(const char *path); @@ -102,6 +102,9 @@ int sim_mkdir(const char *path); int sim_rmdir(const char *path); int sim_samedir(DIR *dirp1, DIR *dirp2); bool sim_dir_exists(const char *dirname); + +struct dirinfo sim_dir_get_info(DIR *dirp, struct sim_dirent *entry); +const char* sim_root_realpath(void); #endif /* !DIRFUNCTIONS_DECLARED */ #endif /* _FILESYSTEM_SIM_H__DIR_H_ */ From 861e53095ff7ea175c853984a3cc58f2bb10e6dc Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Thu, 17 Sep 2026 21:41:42 +0100 Subject: [PATCH 180/408] Fix build errors in 3a57f2f721 Seems that I missed a couple of plugins when renaming 'filesize'. Change-Id: Ifee9daf7d77558c958a192741b2e255dcfb1675e --- apps/plugins/lib/pluginlib_jpeg_load.c | 2 +- apps/plugins/pitch_detector.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/plugins/lib/pluginlib_jpeg_load.c b/apps/plugins/lib/pluginlib_jpeg_load.c index e88f890d0a..bc734f80d9 100644 --- a/apps/plugins/lib/pluginlib_jpeg_load.c +++ b/apps/plugins/lib/pluginlib_jpeg_load.c @@ -23,5 +23,5 @@ #include #include "wrappers.h" -#define filesize rb->filesize +#define ffilesize rb->ffilesize #include "../../recorder/jpeg_load.c" diff --git a/apps/plugins/pitch_detector.c b/apps/plugins/pitch_detector.c index 556bac2841..a8c7944e22 100644 --- a/apps/plugins/pitch_detector.c +++ b/apps/plugins/pitch_detector.c @@ -354,7 +354,7 @@ static void load_settings(void) } /* basic consistency check */ - if(rb->filesize(fd) == sizeof(settings)){ + if(rb->ffilesize(fd) == sizeof(settings)){ rb->read(fd, &settings, sizeof(settings)); rb->memcpy(&hdd_settings, &settings, sizeof(settings)); } From 98a55f623b13c7a06db76c52bd2ac05867dd4bd1 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Thu, 17 Sep 2026 21:59:30 +0100 Subject: [PATCH 181/408] Fix errors in ipod-specific h264_player from 3a57f2f721 Change-Id: I40148d33932eeefb5de73f07104cef78d0739b5d --- apps/plugins/h264_player/mp4_demux.c | 2 +- apps/plugins/h264_player/video_audio.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/plugins/h264_player/mp4_demux.c b/apps/plugins/h264_player/mp4_demux.c index 139b776bff..53fee61c71 100644 --- a/apps/plugins/h264_player/mp4_demux.c +++ b/apps/plugins/h264_player/mp4_demux.c @@ -1353,7 +1353,7 @@ int mp4v_demux_open(const char *filepath, fd = rb->open(filepath, O_RDONLY); if (fd < 0) return -1; - file_size = rb->filesize(fd); + file_size = rb->ffilesize(fd); if (file_size <= 0 || (uint64_t)file_size > UINT32_MAX) { rb->close(fd); diff --git a/apps/plugins/h264_player/video_audio.c b/apps/plugins/h264_player/video_audio.c index 2f8434377e..9bd684907b 100644 --- a/apps/plugins/h264_player/video_audio.c +++ b/apps/plugins/h264_player/video_audio.c @@ -450,7 +450,7 @@ int video_audio_init(const char *filepath, audio_fd = rb->open(filepath, O_RDONLY); if (audio_fd < 0) return -1; - audio_file_size = rb->filesize(audio_fd); + audio_file_size = rb->ffilesize(audio_fd); if (audio_file_size <= 0) { rb->close(audio_fd); From d23a19dc2dafff051a9798a339ac6ba1f563bebb Mon Sep 17 00:00:00 2001 From: Holden McFarland Date: Sat, 19 Sep 2026 07:21:22 -0500 Subject: [PATCH 182/408] ibasso dx50/dx90: Fix physical buttons on yes/no screens The DX50 keymap already defines button_context_yesno, but target_get_context_mapping() did not select it for CONTEXT_YESNOSCREEN. As a result, physical buttons could not operate dialogs such as "Playlist finished. Play again?". Add the missing context mapping. Tested on iBasso DX50 hardware: Play and Previous accept Yes; Next and Power cancel. Change-Id: I9f6078b45a48047a662aad5f231d0adac9a42d5d --- apps/keymaps/keymap-dx50.c | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/keymaps/keymap-dx50.c b/apps/keymaps/keymap-dx50.c index c6bb814469..dd5a0a2e99 100644 --- a/apps/keymaps/keymap-dx50.c +++ b/apps/keymaps/keymap-dx50.c @@ -228,6 +228,7 @@ const struct button_mapping* target_get_context_mapping(int context) case CONTEXT_BOOKMARKSCREEN: { return button_context_bmark; } case CONTEXT_QUICKSCREEN: { return button_context_quickscreen; } case CONTEXT_PITCHSCREEN: { return button_context_pitchscreen; } + case CONTEXT_YESNOSCREEN: { return button_context_yesno; } case CONTEXT_CUSTOM | CONTEXT_SETTINGS: case CONTEXT_SETTINGS_RECTRIGGER: { return button_context_settings_right_is_inc; } case CONTEXT_TREE: From 34a18e7616536fa8187f9551fec306dc76f54fc8 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Sat, 19 Sep 2026 07:51:55 +1000 Subject: [PATCH 183/408] ipodnano3g: NAND driver, FTL and storage The Nano 3G keeps everything behind the S5L8702 flash controller and Apple's FTL ("Whimory"), and nand-nano3g.c was a stub, so the port had no storage at all. This is the flash driver, the FTL and the wiring that makes the NAND Rockbox's disk. The three are one change because neither half is usable without the other: the driver alone cannot see a filesystem, and the FTL alone has nothing to drive. Testing evidence: firmware/target/arm/s5l8702/ipodnano3g/TESTING.md. Co-authored-by: Claude Opus 5 Change-Id: Ib04a991489ff3a1a63cd55552e4a8d7ec7a5fc8c --- bootloader/ipod-s5l87xx.c | 33 + firmware/SOURCES | 1 + firmware/export/config/ipodnano3g.h | 22 +- firmware/export/nand.h | 3 + firmware/export/s5l87xx.h | 4 +- firmware/export/storage.h | 3 + .../target/arm/s5l8702/ipodnano3g/TESTING.md | 16 + .../arm/s5l8702/ipodnano3g/ftl-nano3g.c | 2692 +++++++++++++++++ .../arm/s5l8702/ipodnano3g/ftl-target.h | 60 + .../arm/s5l8702/ipodnano3g/nand-nano3g.c | 1100 ++++++- .../arm/s5l8702/ipodnano3g/nand-target.h | 161 + .../arm/s5l8702/ipodnano3g/pmu-nano3g.c | 3 +- 12 files changed, 4065 insertions(+), 33 deletions(-) create mode 100644 firmware/target/arm/s5l8702/ipodnano3g/TESTING.md create mode 100644 firmware/target/arm/s5l8702/ipodnano3g/ftl-nano3g.c create mode 100644 firmware/target/arm/s5l8702/ipodnano3g/ftl-target.h create mode 100644 firmware/target/arm/s5l8702/ipodnano3g/nand-target.h diff --git a/bootloader/ipod-s5l87xx.c b/bootloader/ipod-s5l87xx.c index 077fa1d3e6..16dbb1cce2 100644 --- a/bootloader/ipod-s5l87xx.c +++ b/bootloader/ipod-s5l87xx.c @@ -45,6 +45,9 @@ #include "version.h" #include "powermgmt.h" #include "usb.h" +#ifdef IPOD_NANO3G +#include "nand-target.h" +#endif #ifdef HAVE_SERIAL #include "serial.h" #endif @@ -878,6 +881,30 @@ void main(void) #endif rc = storage_init(); +#ifdef IPOD_NANO3G + if (rc == NAND_ERR_UNSUPPORTED) { + /* Rockbox only drives NAND chips proven on hardware. Say which + * one this is and how to get it validated, then leave the unit + * to Apple's firmware, which is untouched. */ + lcd_set_foreground(LCD_RBYELLOW); + printf("NAND not supported yet"); + lcd_set_foreground(LCD_WHITE); + printf("chip %08lx x %u", (unsigned long)nand_get_id(), + nand_get_bank_count()); + printf("Rockbox does not write to"); + printf("chips it has not been"); + printf("tested on."); + printf("Starting Apple firmware..."); + sleep(8 * HZ); /* long enough to read the message */ + rc = kernel_launch_onb(); + /* Only reached if the ONB could not be read from NOR */ + printf("Apple firmware failed: %d", rc); + printf("Hold MENU+SELECT to reboot,"); + printf("then SELECT+PLAY for disk mode"); + while (1) + sleep(HZ); + } +#endif if (rc != 0) { printf("Storage error: %d", rc); fatal_error(ERR_STORAGE); @@ -964,6 +991,12 @@ void main(void) printf("P%d T%02x S%llx", i, pinfo.type, (unsigned long long)pinfo.size); } +#if defined(IPOD_NANO3G) && defined(DEFAULT_VIRT_SECTOR_SIZE) + /* Nothing mounted: show USB hosts Apple's 4096-byte sectors, which + * its partition table counts in */ + disk_set_sector_multiplier(IF_MD(0,) + DEFAULT_VIRT_SECTOR_SIZE / SECTOR_SIZE); +#endif fatal_error(ERR_RB); } diff --git a/firmware/SOURCES b/firmware/SOURCES index 02a9d7cd83..5db95ee45a 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1635,6 +1635,7 @@ target/arm/s5l8702/ipodnano3g/adc-nano3g.c target/arm/s5l8702/ipodnano3g/powermgmt-nano3g.c target/arm/s5l8702/ipodnano3g/power-nano3g.c target/arm/s5l8702/ipodnano3g/nand-nano3g.c +target/arm/s5l8702/ipodnano3g/ftl-nano3g.c #if (CONFIG_RTC == RTC_NANO3G) target/arm/s5l8702/ipodnano3g/rtc-nano3g.c // TODO: -> drivers/rtc/rtc_xxx.c ??? #endif diff --git a/firmware/export/config/ipodnano3g.h b/firmware/export/config/ipodnano3g.h index 9583b3b3d2..6bcb05f2e3 100644 --- a/firmware/export/config/ipodnano3g.h +++ b/firmware/export/config/ipodnano3g.h @@ -80,6 +80,12 @@ #define CONFIG_STORAGE STORAGE_NAND +/* The flash controller DMAs straight into and out of the caller's buffer + * and only handles word-aligned ones: a read into a buffer 1-3 bytes off + * returns wrong data and writes past the buffer (measured). The FAT code + * bounces unaligned transfers, as on the Classic 6G. */ +#define STORAGE_NEEDS_BOUNCE_BUFFER + // TODO //#define CONFIG_NAND NAND_SAMSUNG #define CONFIG_NAND 0 @@ -88,9 +94,18 @@ needs to do cleanup on shutdown */ #define HAVE_STORAGE_FLUSH +/* Chips not validated are mounted read-only; USB hosts are told so */ +#define HAVE_STORAGE_READONLY + /* The NAND flash has 2048-byte sectors, and is our only storage */ #define SECTOR_SIZE 2048 +/* Apple's partition table on the NAND counts 4096-byte sectors */ +#define MAX_VIRT_SECTOR_SIZE 4096 + +/* If we have no valid partitions, advertise this as our sector size */ +#define DEFAULT_VIRT_SECTOR_SIZE 4096 + /* LCD dimensions */ #define LCD_WIDTH 320 #define LCD_HEIGHT 240 @@ -122,8 +137,8 @@ /* Define this to enable morse code input */ #define HAVE_MORSE_INPUT -// TODO /* define this if you have a real-time clock */ +// TODO //#define CONFIG_RTC RTC_NANO3G #define CONFIG_RTC 0 @@ -135,7 +150,7 @@ // TODO #if 0 /* Define the type of audio codec */ -#define HAVE_WM1870 +#define HAVE_WM8975 #endif // XXX: dummy for preliminary build, WRONG CODEC!!! #define HAVE_CS42L55 @@ -238,8 +253,7 @@ #define USB_VENDOR_ID 0x05AC #define USB_PRODUCT_ID 0x1262 #define USB_DEVBSS_ATTR __attribute__((aligned(32))) -// TODO -//#define HAVE_BOOTLOADER_USB_MODE +#define HAVE_BOOTLOADER_USB_MODE #ifdef BOOTLOADER #define USBPOWER_BTN_IGNORE (~0) #endif diff --git a/firmware/export/nand.h b/firmware/export/nand.h index bf597f5f6a..43e24da961 100644 --- a/firmware/export/nand.h +++ b/firmware/export/nand.h @@ -45,6 +45,9 @@ int nand_spinup_time(void); /* ticks */ #ifdef STORAGE_GET_INFO void nand_get_info(IF_MD(int drive,) struct storage_info *info); #endif +#ifdef HAVE_STORAGE_READONLY +bool nand_readonly(IF_MD_NONVOID(int drive)); +#endif long nand_last_disk_activity(void); diff --git a/firmware/export/s5l87xx.h b/firmware/export/s5l87xx.h index 2dcef72960..6d51282ae1 100644 --- a/firmware/export/s5l87xx.h +++ b/firmware/export/s5l87xx.h @@ -929,7 +929,7 @@ #define RSSYND3_2 (*(REG32_PTR_T)(FMC_BASE + 0x0188)) /* On-the-fly Synd Register3[71:64] */ #define FLAGSYND (*(REG32_PTR_T)(FMC_BASE + 0x0190)) /* On-the-fly ECC Result Flag */ -/* S5L8702 registers used by Apple's NANDReadPage (BootROM 0x20009910). +/* S5L8702 registers the BootROM's page read uses. * The real names are unknown. */ #define FMUNK78 (*(REG32_PTR_T)(FMC_BASE + 0x0078)) /* spare decode setup, 0x5140 */ #define FMUNK7C (*(REG32_PTR_T)(FMC_BASE + 0x007C)) /* bit 1 start/busy; FMSYND5..7 */ @@ -938,6 +938,8 @@ #define FMTRANS1 (*(REG32_PTR_T)(FMC_BASE + 0x0814)) /* correction mode, always 0x01000180 */ #define FMTRANSSTAT (*(REG32_PTR_T)(FMC_BASE + 0x0840)) /* correction status, w1c; bit 2 done */ +#define FMCTRL0_CE(bank) (1 << ((bank) + 1)) /* chip enable bits 1-8 */ +#define FMCTRL0_CE_MASK (0xff << 1) #define FMCTRL0_ENABLEDMA (1 << 10) #define FMCTRL0_UNK1 (1 << 11) #define FMCTRL0_AUTOXFER (1 << 24) /* S5L8702: write the chunk to the address in FMDATAW0 */ diff --git a/firmware/export/storage.h b/firmware/export/storage.h index 85c1ed9434..6cd52f9336 100644 --- a/firmware/export/storage.h +++ b/firmware/export/storage.h @@ -266,6 +266,9 @@ static inline void storage_sleep(void) {}; #define storage_removable(drive) nand_removable(IF_MD(drive)) #define storage_present(drive) nand_present(IF_MD(drive)) #endif + #ifdef HAVE_STORAGE_READONLY + #define storage_readonly(drive) nand_readonly(IF_MD(drive)) + #endif #define storage_driver_type(drive) (STORAGE_NAND_NUM) #elif (CONFIG_STORAGE & STORAGE_RAMDISK) #define STORAGE_FUNCTION(NAME) (ramdisk_## NAME) diff --git a/firmware/target/arm/s5l8702/ipodnano3g/TESTING.md b/firmware/target/arm/s5l8702/ipodnano3g/TESTING.md new file mode 100644 index 0000000000..318110ed84 --- /dev/null +++ b/firmware/target/arm/s5l8702/ipodnano3g/TESTING.md @@ -0,0 +1,16 @@ +# iPod Nano 3G testing notes + +Evidence for the driver, FTL, storage and hardware support in this +directory. + +## NAND driver, FTL and storage + +Since the earlier host and hardware pass: `test_disk` passes at every +buffer alignment - it's what found and confirmed the fix for a DMA +alignment bug (a misaligned buffer returned wrong data and wrote past +itself). The storage thread holds the FTL for at most 492 ms during a +merge, measured with a timing build - within the 3 s PCM buffer, so no +audio starvation. + +The 8GB Hynix A555D5AD row's evidence is Robin Tschirschnitz's (delacor): +measured and run on their own unit, not this one. diff --git a/firmware/target/arm/s5l8702/ipodnano3g/ftl-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/ftl-nano3g.c new file mode 100644 index 0000000000..393aa60324 --- /dev/null +++ b/firmware/target/arm/s5l8702/ipodnano3g/ftl-nano3g.c @@ -0,0 +1,2692 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2009 by Michael Sparmann + * + * The log-block handling, the control-block list and the VFL context commit + * follow firmware/target/arm/s5l8700/ipodnano2g/ftl-nano2g.c, which decodes + * the same Whimory structures on the Nano 2G. Several functions are ports of + * their counterparts there. + * + * Copyright (C) 2026 by Andrew Rice + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* + * FTL for the iPod Nano 3G: Apple's Whimory, in the variant this firmware + * uses. It is related to the Nano 2G's (ftl-nano2g.c), but the VFL context + * layout differs and a virtual block groups one to four physical blocks per + * chip enable, as the chip's mode lays them out. + * + * + * From the bottom up: + * - VFL: each bank's blocks form planes units ("planes"); virtual block b + * of a unit is a physical block placed by the chip's mode + * (ftl_unit_block()), unless that bank's VFL context remaps it. The + * context holds one table of vflspares slots per unit. Slot k holds the + * physical block it replaces (0xfff0 free, 0xffff unusable) and stands + * for the reserved virtual block nsuperblocks + k % vflspares of unit + * k / vflspares, as Apple's VFL numbers them. + * - Superblocks: superblock s is virtual block s of every unit of every + * bank. Its pages are numbered v = page * banks * planes + plane * banks + * + bank. + * - FTL: logical page lpn is at index lpn % sbpages of logical block + * lpn / sbpages. The block map, committed in the FTL control blocks, says + * which superblock holds each logical block. Pages written since a block + * was last merged are in log superblocks, which the context and its log + * offset tables record at every commit; a page present in a log is newer + * than the one the map points at, and ftl_resolve() prefers it. + * + * The write path follows the original firmware's own FTL: writes append to + * logs, a merge folds or compacts them when they fill or their slot is + * needed, and ftl_sync() folds every log and commits, as the firmware's + * flush does. Nothing else commits, so a mount that finds writes after the + * last commit rebuilds the tables from the medium, as the firmware's + * restore does. Pages go to the flash in runs, the way the firmware hands + * them to the VFL. + * A block that will not take a write or an erase is replaced from the VFL's + * reserved spares. + */ + +#include "config.h" +#include "system.h" +#include "kernel.h" +#include "logf.h" +#include "panic.h" +#include "storage.h" +#include "nand-target.h" +#include "ftl-target.h" +#include +#include +#include + +#define FTL_MAX_BANKS 4 +/* Sized for the largest chip in nand_chip_table[]: the 8GB parts give 3895 + * superblocks and 3872 logical blocks. The 4GB one needs half of that. */ +#define FTL_MAX_SB 4096 /* superblocks in the virtual area */ +#define FTL_MAX_USERSB 4096 /* logical blocks */ +#define FTL_MAX_SBPAGES 1024 /* pages per superblock */ +#define FTL_MAX_LOGS 17 /* Whimory keeps 0x11 log blocks */ +#define FTL_MAP_PAGES (FTL_MAX_USERSB * 2 / NAND_PAGE_SIZE) +#define FTL_VFL_SCANBLOCKS 200 /* where to look for a VFL context */ +#define FTL_NO_PAGE 0xffff +#define FTL_VFL_FREE 0xfff0 /* a reserved block nothing uses yet */ +#define FTL_VFL_BAD 0xffff /* a reserved block that is itself bad */ +#define FTL_VFL_PENDING 20 /* Apple's pending-bad list length */ + +/* Spare metadata types */ +#define SPARE_DATA 0x40 +#define SPARE_DATA_LAST 0x41 /* last page of a superblock */ +#define SPARE_FTL_CXT 0x43 +#define SPARE_FTL_MAP 0x44 +#define SPARE_FTL_LOG 0x45 /* log page-offset table */ +#define SPARE_FTL_ERASECTR 0x46 +#define SPARE_FTL_STATS 0x47 +#define SPARE_FTL_READCOUNT 0x49 +#define SPARE_FTL_MOUNT 0x4f /* "written to since the last commit" */ +#define SPARE_VFL_CXT 0x80 +#define SPARE_ERASED 0xff + +/* The FTL context, as it sits in its flash page. The named fields are the + * ones we understand; the pad arrays keep everything else exactly as we + * found it, so a commit of ours preserves what the original firmware + * expects. Field offsets were derived from the firmware's own context load + * and store, and the FTL_CHECK assertions below pin every one of them. */ +#define FTL_POOL_SIZE 20 /* spare superblocks: free pool + logs */ +#define FTL_CXT_LOGS 18 /* log slots in the context */ +#define FTL_CXT_PAGELIST 36 /* entries in the side-table page lists */ + +struct ftl_cxt_log +{ + uint32_t usn; + uint16_t sb; + uint16_t lblock; + uint32_t offsetsptr; /* Whimory's RAM pointer, preserved unused */ + uint16_t pagesused; + uint16_t pagescurrent; + uint32_t issequential; +}; + +struct ftl_cxt +{ + uint32_t usn; /* decrements once per control page */ + uint32_t maxusn; /* highest user-data usn */ + uint16_t freecount; /* live entries in blockpool */ + uint16_t nextfreeidx; /* head of the free ring */ + uint16_t swapcounter; /* wear-levelling budget: +1 per merge */ + uint16_t blockpool[FTL_POOL_SIZE]; + uint8_t field_36[2]; + uint32_t mappages[18]; /* vpages holding the block map */ + uint32_t ecpages[FTL_CXT_PAGELIST]; /* ... the erase counters */ + uint32_t logpages[FTL_MAX_LOGS]; /* ... the log offset tables */ + uint8_t field_154[0x44]; + uint32_t mapptr; /* three more of Whimory's RAM pointers */ + uint32_t ecptr; + uint32_t logoffsptr; + struct ftl_cxt_log logs[FTL_CXT_LOGS]; + uint8_t field_30c[6]; + uint16_t ctrlblocks[3]; + uint32_t ctrlpage; + uint32_t cleanflag; /* set by a commit, cleared by a write */ + uint32_t rcpages[FTL_CXT_PAGELIST]; /* ... the read counters */ + uint32_t rcptr; + uint8_t field_3b4[0x1c]; + uint32_t statspage; + uint32_t statsflag; /* 0xffffffff = statspage is set */ + uint8_t field_3d8[0x428]; +}; + +#define FTL_SIDE_ENTRIES (FTL_MAX_SB + 32) /* per-superblock tables */ +#define FTL_SIDE_PAGES ((FTL_SIDE_ENTRIES * 2 + NAND_PAGE_SIZE - 1) \ + / NAND_PAGE_SIZE) +#define FTL_LOG_PAGES ((FTL_MAX_SBPAGES * FTL_MAX_LOGS * 2 \ + + NAND_PAGE_SIZE - 1) / NAND_PAGE_SIZE) +/* The page counts above are at the smallest page size. A table is read and + * written a whole page at a time, so its buffer is rounded up to the + * largest. */ +#define FTL_WHOLE_PAGES(bytes) (((bytes) + NAND_MAX_PAGE_SIZE - 1) \ + / NAND_MAX_PAGE_SIZE * NAND_MAX_PAGE_SIZE) +#define FTL_SIDE_BYTES FTL_WHOLE_PAGES(FTL_SIDE_ENTRIES * 2) +#define FTL_LOG_BYTES FTL_WHOLE_PAGES(FTL_MAX_SBPAGES * FTL_MAX_LOGS * 2) +/* Pages one commit occupies: erase counters, read counters, map, log + * tables, stats, then the context itself */ +#define FTL_COMMIT_PAGES(ec, rc, map, log) ((ec) + (rc) + (map) + (log) + 2) + +/* VFL context, one per bank: nano2g's header, then per-plane remap tables */ +struct ftl_vfl_cxt +{ + uint32_t usn; /* cross-bank update sequence number */ + uint16_t ftlctrlblocks[3]; /* superblocks holding FTL control pages */ + uint16_t field_a; + uint32_t updatecount; /* decrementing, also in the spare */ + uint16_t activecxtblock; + uint16_t nextcxtpage; + uint16_t badcount; /* slots marked 0xffff, i.e. spares that + * are themselves bad - not a count of + * remapped blocks (measured) */ + uint16_t field_16[3]; + uint16_t usedcount[4]; /* slots taken in each unit's table */ + uint16_t field_24[4]; + uint16_t remap[820]; /* plane tables, vflspares entries each */ + uint16_t vflcxtblocks[4]; /* physical blocks of the context ring */ + uint16_t pendingcount; /* blocks awaiting replacement */ + uint16_t pending[FTL_VFL_PENDING]; /* physical blocks that failed a read + * or write, replaced at their next + * erase */ + uint8_t field_6c6[0x12e]; + uint32_t version; + uint32_t checksum1; + uint32_t checksum2; +}; + +#define FTL_CHECK(name, cond) typedef char name[(cond) ? 1 : -1] +FTL_CHECK(ftl_vfl_cxt_size, sizeof(struct ftl_vfl_cxt) == 0x800); +FTL_CHECK(ftl_vfl_cxt_remap, offsetof(struct ftl_vfl_cxt, remap) == 0x2c); +FTL_CHECK(ftl_vfl_cxt_blocks, + offsetof(struct ftl_vfl_cxt, vflcxtblocks) == 0x694); +FTL_CHECK(ftl_vfl_cxt_pending, + offsetof(struct ftl_vfl_cxt, pending) == 0x69e); +FTL_CHECK(ftl_vfl_cxt_cks, offsetof(struct ftl_vfl_cxt, checksum1) == 0x7f8); +FTL_CHECK(ftl_cxt_size, sizeof(struct ftl_cxt) == 0x800); +FTL_CHECK(ftl_cxt_log_size, sizeof(struct ftl_cxt_log) == 20); +FTL_CHECK(ftl_cxt_pool, offsetof(struct ftl_cxt, blockpool) == 0x00e); +FTL_CHECK(ftl_cxt_map, offsetof(struct ftl_cxt, mappages) == 0x038); +FTL_CHECK(ftl_cxt_ec, offsetof(struct ftl_cxt, ecpages) == 0x080); +FTL_CHECK(ftl_cxt_logp, offsetof(struct ftl_cxt, logpages) == 0x110); +FTL_CHECK(ftl_cxt_logs, offsetof(struct ftl_cxt, logs) == 0x1a4); +FTL_CHECK(ftl_cxt_ctrl, offsetof(struct ftl_cxt, ctrlblocks) == 0x312); +FTL_CHECK(ftl_cxt_clean, offsetof(struct ftl_cxt, cleanflag) == 0x31c); +FTL_CHECK(ftl_cxt_rc, offsetof(struct ftl_cxt, rcpages) == 0x320); +FTL_CHECK(ftl_cxt_stats, offsetof(struct ftl_cxt, statspage) == 0x3d0); + +/* A log block: pages of one logical block written since the last commit, + * scattered through a superblock of their own. Fields follow + * ftl-nano2g.c's struct ftl_log_type, which is the same structure Apple + * keeps at context offset 0x1a4. */ +struct ftl_log +{ + uint32_t usn; + uint16_t sb; /* the scattered superblock */ + uint16_t lblock; /* the logical block it stands for */ + uint16_t pagesused; /* append cursor */ + uint16_t pagescurrent; /* pages still live, i.e. to copy */ + uint32_t issequential; /* still written in order? */ + uint16_t offsets[FTL_MAX_SBPAGES]; /* index in lblock -> v in sb */ +}; + +/* Source of replacement page data for ftl_write_merge(): a pointer to a + * page of data for lpn, or NULL to keep what is there now. */ +typedef const void *(*ftl_write_src)(uint32_t lpn, void *ctx); + +static const struct nand_geometry *geo; +static uint32_t nsuperblocks; /* superblocks in the virtual area */ +static uint32_t sbpages; /* pages per superblock */ +static uint32_t usersb; /* logical blocks */ +static uint32_t pagesize; /* bytes per NAND page */ +static uint32_t secperpage; /* NAND_PAGE_SIZE sectors in one page */ +static bool ftl_mounted = false; +static struct mutex ftl_mtx; + +static struct ftl_vfl_cxt ftl_vfl_cxt[FTL_MAX_BANKS]; +static uint16_t ftl_map[FTL_MAX_USERSB]; +static uint16_t ftl_ctrl[3]; +static struct ftl_log ftl_logs[FTL_MAX_LOGS]; +/* A page-sized scratch for the log offset tables, which a clean load reads + * and a commit writes - never both at once, so they share it */ +static uint16_t ftl_logtable[FTL_LOG_BYTES / 2] STORAGE_ALIGN_ATTR; + +/* + * The buffers here and below are sized for the largest chip in the driver's + * table, since storage is up long before there is an allocator to ask. They + * come to about 540KB, 2% of this target's 28MB audio buffer. Overlapping + * the mount-time ones (ftl_rs, the restore's log slots) with ftl_copybuf + * would save 68KB, and halving the copy batch another 128KB, but the first + * trades a user's flash against an aliasing invariant and the second slows + * every merge; 0.4% of the buffer does not pay for either. ftl_copybuf's + * size is one constant (FTL_COPY_PAGES) for anyone who disagrees. + */ +static unsigned int ftl_nlogs; + +static uint8_t ftl_buffer[NAND_MAX_PAGE_SIZE] STORAGE_ALIGN_ATTR; +static uint32_t ftl_meta[NAND_META_WORDS]; + +/* Write-path state. The committed context page is kept verbatim so that a + * commit of ours preserves every field we do not understand. */ +static bool ftl_writable = false; +static bool ftl_readonly = false; /* not writable since the mount */ +static struct ftl_cxt ftl_cxt STORAGE_ALIGN_ATTR; +static uint16_t ftl_erasectr[FTL_SIDE_BYTES / 2]; +static uint16_t ftl_readcount[FTL_SIDE_BYTES / 2]; +static uint8_t ftl_stats[NAND_MAX_PAGE_SIZE] STORAGE_ALIGN_ATTR; +/* A 0x800-byte context padded out to a whole page for writing */ +static uint8_t ftl_cxtpage[NAND_MAX_PAGE_SIZE] STORAGE_ALIGN_ATTR; +/* A page holding sectors a read or write covers only part of */ +static uint8_t ftl_sectorpage[NAND_MAX_PAGE_SIZE] STORAGE_ALIGN_ATTR; +static uint32_t ftl_vfl_usn; /* cross-bank VFL context update counter */ +static uint32_t ftl_ctrl_idx; /* which of ftl_ctrl[] is being written */ +static uint32_t ftl_ctrl_v; /* next free page in it */ +static uint32_t ftl_necpages, ftl_nrcpages, ftl_nmappages, ftl_nlogpages; + +/* The run of pages being gathered for one program call (see ftl_run_add) */ +static struct nand_write ftl_run[FTL_MAX_SBPAGES]; +static uint32_t ftl_run_meta[FTL_MAX_SBPAGES][NAND_META_WORDS]; +static uint32_t ftl_run_sb, ftl_run_v, ftl_run_n; +/* Merges read a batch of pages, then write it: an eighth of a superblock + * at a time, as the firmware's simple, copy and move merges do. + * The buffer holds that at any page size, since ftl_init() refuses a + * superblock larger than FTL_MAX_SBPAGES pages of NAND_PAGE_SIZE. */ +#define FTL_COPY_PAGES (FTL_MAX_SBPAGES / 8) +static uint8_t ftl_copybuf[FTL_COPY_PAGES * NAND_PAGE_SIZE] STORAGE_ALIGN_ATTR; +static struct nand_read ftl_read_run[FTL_MAX_SBPAGES]; +static uint32_t ftl_read_meta[FTL_MAX_SBPAGES][NAND_META_WORDS]; + +static uint32_t meta_type(const uint32_t *meta) +{ + return (meta[2] >> 8) & 0xff; +} + +static bool meta_is_data(const uint32_t *meta) +{ + return meta_type(meta) == SPARE_DATA || meta_type(meta) == SPARE_DATA_LAST; +} + +/* Read a page into ftl_buffer and ftl_meta. Returns its spare type, or -1 */ +static int ftl_raw_read(uint32_t bank, uint32_t page) +{ + int rc = nand_read_page(bank, page, ftl_buffer, ftl_meta); + if (rc < 0 || rc == NAND_ECC_FAILED) + return -1; + return meta_type(ftl_meta); +} + +static void ftl_vfl_calculate_checksum(const struct ftl_vfl_cxt *cxt, + uint32_t *sum, uint32_t *x) +{ + const uint32_t *w = (const uint32_t *)cxt; + unsigned int i; + + *sum = 0xAABBCCDD; + *x = 0xAABBCCDD; + for (i = 0; i < offsetof(struct ftl_vfl_cxt, checksum1) / 4; i++) + { + *sum += w[i]; + *x ^= w[i]; + } +} + +static bool ftl_vfl_checksum_ok(const struct ftl_vfl_cxt *cxt) +{ + uint32_t sum, x; + + ftl_vfl_calculate_checksum(cxt, &sum, &x); + return sum == cxt->checksum1 && x == cxt->checksum2; +} + +/* A VFL context commit is 8 copies. Load the first good one at or after + * page into ftl_buffer. */ +static int ftl_vfl_read_cxt(uint32_t bank, uint32_t block, uint32_t page) +{ + uint32_t i; + + for (i = page; i < page + 8 && i < geo->pagesperblock; i++) + if (ftl_raw_read(bank, block * geo->pagesperblock + i) + == SPARE_VFL_CXT + && (ftl_meta[2] & 0xff) == 0 + && ftl_vfl_checksum_ok((const struct ftl_vfl_cxt *)ftl_buffer)) + return 0; + return -1; +} + +/* Find the newest VFL context of a bank, as nano2g's ftl_vfl_open() does */ +static int ftl_vfl_open_bank(uint32_t bank) +{ + uint16_t ring[4]; + uint32_t i, block, last, best = 4, bestusn = 0xffffffff; + + /* Any context lists the blocks the context ring lives in */ + for (block = 1; block < FTL_VFL_SCANBLOCKS; block++) + if (!ftl_vfl_read_cxt(bank, block, 0)) + break; + if (block == FTL_VFL_SCANBLOCKS) + return -1; + memcpy(ring, ((const struct ftl_vfl_cxt *)ftl_buffer)->vflcxtblocks, + sizeof(ring)); + + /* The ring's newest block has the lowest update count */ + for (i = 0; i < 4; i++) + { + if (ring[i] >= geo->blocks || ftl_vfl_read_cxt(bank, ring[i], 0)) + continue; + if (ftl_meta[0] && ftl_meta[0] <= bestusn) + { + bestusn = ftl_meta[0]; + best = i; + } + } + if (best == 4) + return -1; + + /* Its last commit is the newest context */ + block = ring[best]; + last = 0; + for (i = 8; i < geo->pagesperblock; i += 8) + { + if (ftl_vfl_read_cxt(bank, block, i)) + break; + last = i; + } + if (ftl_vfl_read_cxt(bank, block, last)) + return -1; + memcpy(&ftl_vfl_cxt[bank], ftl_buffer, sizeof(struct ftl_vfl_cxt)); + return 0; +} + +/* The physical block of virtual block vblock of a bank's unit, as the + * original firmware's per-mode address functions place it */ +static uint32_t ftl_unit_block(uint32_t unit, uint32_t vblock) +{ + uint32_t half = geo->blocks / 2; + + switch (geo->layout) + { + case NAND_LAYOUT_SINGLE: + return vblock; + case NAND_LAYOUT_HALVES: + return vblock + unit * half; + case NAND_LAYOUT_BOTH: + return 2 * vblock + (unit & 1) + ((unit & 2) ? half : 0); + case NAND_LAYOUT_SPLIT13: + /* 8320 blocks: two 4096-block regions halved, then 128 blocks in + * four runs of 32 */ + if (vblock < 4096) + return vblock + (vblock >= 2048 ? 2048 : 0) + unit * 2048; + if (vblock < 4128) + return 8192 + unit * 32 + (vblock - 4096); + return 8256 + unit * 32 + (vblock - 4128); + default: /* NAND_LAYOUT_ADJACENT */ + return 2 * vblock + unit; + } +} + +/* The physical block of slot k's reserved block */ +static uint32_t ftl_vfl_spare_block(uint32_t k) +{ + return ftl_unit_block(k / geo->vflspares, + nsuperblocks + k % geo->vflspares); +} + +/* Check that a bank's VFL context is laid out as the chip table row says. + * Each unit's table is vflspares slots: the physical blocks it has + * replaced and bad spares, then from its first free slot only free and bad + * ones, and the rest of the array is zero (measured on the 4GB unit). A + * wrong reserved count moves those boundaries, so this refuses a row that + * does not describe the chip rather than mount it with the wrong map. */ +static int ftl_vfl_check_cxt(const struct ftl_vfl_cxt *cxt) +{ + uint32_t unit, i, v; + bool freeseen; + + for (i = 0; i < 4; i++) + if (cxt->vflcxtblocks[i] >= geo->blocks) + return -1; + for (i = 0; i < 3; i++) + if (cxt->ftlctrlblocks[i] >= nsuperblocks) + return -1; + for (unit = 0; unit < geo->planes; unit++) + { + const uint16_t *table = &cxt->remap[unit * geo->vflspares]; + + if (cxt->usedcount[unit] > geo->vflspares) + return -1; + freeseen = false; + for (i = 0; i < geo->vflspares; i++) + { + v = table[i]; + if (v == FTL_VFL_FREE) + freeseen = true; + else if (v != FTL_VFL_BAD && (freeseen || v >= geo->blocks)) + return -1; + } + } + for (i = geo->planes * geo->vflspares; i < ARRAYLEN(cxt->remap); i++) + if (cxt->remap[i]) + return -1; + return 0; +} + +/* Where virtual block vblock of a unit really is: its own physical block, + * or the reserved block a slot of any unit replaced it with - a unit whose + * table is full borrows another's, so Apple's lookup scans them all */ +static uint32_t ftl_vfl_phys_block(uint32_t bank, uint32_t unit, + uint32_t vblock) +{ + const uint16_t *remap = ftl_vfl_cxt[bank].remap; + uint32_t block = ftl_unit_block(unit, vblock); + uint32_t k; + + for (k = 0; k < geo->planes * geo->vflspares; k++) + if (remap[k] == block) + return ftl_vfl_spare_block(k); + return block; +} + +static void ftl_vpage_phys(uint32_t sb, uint32_t v, + uint32_t *bank, uint32_t *page) +{ + uint32_t plane = (v / geo->banks) % geo->planes; + + *bank = v % geo->banks; + *page = ftl_vfl_phys_block(*bank, plane, sb) * geo->pagesperblock + + v / (geo->banks * geo->planes); +} + +/* Read page v of superblock sb into ftl_buffer and ftl_meta */ +static int ftl_read_vpage(uint32_t sb, uint32_t v) +{ + uint32_t bank, page; + + ftl_vpage_phys(sb, v, &bank, &page); + return ftl_raw_read(bank, page); +} + +static uint32_t ftl_vpn(uint32_t sb, uint32_t v) +{ + return sb * sbpages + v; +} + +/* The spare types that live in FTL control blocks */ +static bool type_is_ctrl(int type) +{ + return type >= SPARE_FTL_CXT && type <= SPARE_FTL_MOUNT; +} + +/* Find the FTL context, as the firmware's context load does: the newest control + * block is the one whose page 0 has the lowest usn, and its last written + * page must be a context. Returns 1 if it is - the context then describes + * the medium exactly, open logs included - 0 if not, in which case the + * medium has changed since that commit and ftl_restore() must rebuild it, + * or a negative number if no context exists at all. Either way ftl_cxt + * holds the newest context page found, so a commit of ours preserves the + * fields we do not understand. */ +static int ftl_load_cxt(void) +{ + const struct ftl_vfl_cxt *newest = &ftl_vfl_cxt[0]; + uint32_t i, v, ctrl = 3, cxtv = FTL_MAX_SBPAGES; + uint32_t minusn = 0xffffffff, page0usn = 0xffffffff, newblk = 3; + uint32_t used[3] = { 0, 0, 0 }; + int type, lasttype[3] = { -1, -1, -1 }; + bool clean; + + for (i = 1; i < geo->banks; i++) + if (ftl_vfl_cxt[i].usn > newest->usn) + newest = &ftl_vfl_cxt[i]; + memcpy(ftl_ctrl, newest->ftlctrlblocks, sizeof(ftl_ctrl)); + + for (i = 0; i < 3; i++) + { + if (ftl_ctrl[i] >= nsuperblocks) + continue; + for (v = 0; v < sbpages; v++) + { + type = ftl_read_vpage(ftl_ctrl[i], v); + if (type == SPARE_ERASED) + break; + if (v == 0 && type_is_ctrl(type) && ftl_meta[0] < page0usn) + { + page0usn = ftl_meta[0]; + newblk = i; + } + if (type == SPARE_FTL_CXT && ftl_meta[0] <= minusn) + { + minusn = ftl_meta[0]; + ctrl = i; + cxtv = v; + } + lasttype[i] = type; + } + used[i] = v; + } + /* Clean: the newest block ends in a context. Otherwise keep the + * newest context anywhere, for its fields. */ + clean = newblk < 3 && lasttype[newblk] == SPARE_FTL_CXT; + if (clean) + { + ctrl = newblk; + cxtv = used[newblk] - 1; + } + if (ctrl == 3 || ftl_read_vpage(ftl_ctrl[ctrl], cxtv) != SPARE_FTL_CXT) + return -2; + memcpy(&ftl_cxt, ftl_buffer, sizeof(ftl_cxt)); + ftl_cxt.usn = ftl_meta[0]; + ftl_ctrl_idx = ctrl; + ftl_ctrl_v = used[ctrl]; + return clean; +} + +/* Read npages pages of a context pointer list into table, entries bytes */ +static int ftl_read_list(const uint32_t *list, uint32_t npages, int type, + void *table, uint32_t bytes) +{ + uint32_t i, vpn; + + for (i = 0; i < npages; i++) + { + vpn = list[i]; + if (vpn / sbpages >= nsuperblocks + || ftl_read_vpage(vpn / sbpages, vpn % sbpages) != type + || (ftl_meta[1] & 0xffff) != i) + return -1; + memcpy((uint8_t *)table + i * pagesize, ftl_buffer, + MIN(bytes - i * pagesize, pagesize)); + } + return 0; +} + +/* The rest of a clean load: map, open logs and free pool, all as the + * context records them */ +static int ftl_load_clean(void) +{ + uint32_t i, sb; + + if (ftl_read_list(ftl_cxt.mappages, + (usersb * 2 + pagesize - 1) / pagesize, + SPARE_FTL_MAP, + ftl_map, usersb * 2)) + return -3; + for (i = 0; i < usersb; i++) + if (ftl_map[i] >= nsuperblocks) + return -4; + + /* One offset table of sbpages entries per log slot, in slot order */ + if (ftl_read_list(ftl_cxt.logpages, + (sbpages * FTL_MAX_LOGS * 2 + pagesize - 1) + / pagesize, + SPARE_FTL_LOG, ftl_logtable, sbpages * FTL_MAX_LOGS * 2)) + return -5; + ftl_nlogs = 0; + for (i = 0; i < FTL_MAX_LOGS; i++) + { + const struct ftl_cxt_log *c = &ftl_cxt.logs[i]; + const uint16_t *offsets = ftl_logtable + i * sbpages; + struct ftl_log *log; + uint32_t idx; + + if (c->sb == 0xffff) + continue; + if (c->sb >= nsuperblocks || c->lblock >= usersb + || c->pagesused > sbpages || c->pagescurrent > sbpages) + return -6; + for (idx = 0; idx < sbpages; idx++) + if (offsets[idx] != FTL_NO_PAGE && offsets[idx] >= sbpages) + return -6; + log = &ftl_logs[ftl_nlogs++]; + log->usn = c->usn; + log->sb = c->sb; + log->lblock = c->lblock; + log->pagesused = c->pagesused; + log->pagescurrent = c->pagescurrent; + log->issequential = c->issequential; + memcpy(log->offsets, offsets, sbpages * 2); + } + + if (ftl_cxt.freecount > FTL_POOL_SIZE + || ftl_cxt.nextfreeidx >= FTL_POOL_SIZE) + return -7; + for (i = 0; i < ftl_cxt.freecount; i++) + { + sb = ftl_cxt.blockpool[(ftl_cxt.nextfreeidx + i) % FTL_POOL_SIZE]; + if (sb >= nsuperblocks) + return -7; + } + return 0; +} + +/* ---- Rebuilding the tables from the medium --------------------------- */ +/* Rebuilds the map, the open logs and the free pool from the medium alone, + * when the newest control page is not a context. Every phase follows the + * decode of the original firmware's restore; the letters are that decode's + * phases, and each one is a ftl_restore_*() of its own below, called in + * order by ftl_restore(). The letter is on the call and on the function, so + * the correspondence with the decode notes stays readable. Nothing is + * written here: the caller erases the hole blocks and the pool and commits + * (phases D, H and J) when the mount is writable. */ + +#define RS_LOG 0x40 +#define RS_DATA 0x41 +#define RS_CTRL 0x42 +#define RS_FREE 0x48 + +struct ftl_rs +{ + uint16_t lblock; + uint8_t state; + uint32_t usn; +}; +static struct ftl_rs ftl_rs[FTL_MAX_SB]; +static uint8_t ftl_hole[FTL_MAX_USERSB / 8 + 1]; /* map given an unerased + * free block */ +static uint32_t ftl_nholes; + +static bool ftl_is_hole(uint32_t lblock) +{ + return ftl_hole[lblock / 8] & (1 << (lblock % 8)); +} + +/* The firmware's promotion step: a log whose every readable page sits at + * its own index can stand as the data block */ +static bool ftl_log_in_place(uint32_t sb) +{ + uint32_t v; + + int type; + + for (v = 0; v < sbpages; v++) + { + type = ftl_read_vpage(sb, v); + if (type >= 0 && type != SPARE_ERASED && ftl_meta[0] % sbpages != v) + return false; + } + return true; +} + +/* the firmware's log scan: every page of the block, the last copy of each + * index winning */ +static void ftl_scan_log(struct ftl_log *log) +{ + uint32_t v, idx; + int type; + + memset(log->offsets, 0xff, sbpages * sizeof(log->offsets[0])); + log->pagescurrent = 0; + for (v = 0; v < sbpages; v++) + { + type = ftl_read_vpage(log->sb, v); + if (type < 0 || type == SPARE_ERASED) + continue; + idx = ftl_meta[0] % sbpages; + if (log->offsets[idx] == FTL_NO_PAGE) + log->pagescurrent++; + log->offsets[idx] = v; + } +} + +/* Phase C hands one slot to each log it keeps, and phase E drains the slots + * into ftl_logs[]. The array is static because struct ftl_log is about 2KB + * and eighteen of them will not fit on the mount's stack; it is live only + * between ftl_restore_place_logs() and ftl_restore_build_pool(). */ +#define FTL_RS_SLOTS (FTL_MAX_LOGS + 1) +static struct ftl_log ftl_rs_slots[FTL_RS_SLOTS]; + +/* phase A: the control blocks; the newest by page 0 holds the next commit's + * predecessor, and the next commit starts a fresh block */ +static int ftl_restore_ctrl_blocks(void) +{ + uint32_t i, page0usn = 0xffffffff; + + for (i = 0; i < 3; i++) + { + if (ftl_ctrl[i] >= nsuperblocks) + return -30; + ftl_rs[ftl_ctrl[i]].state = RS_CTRL; + if (ftl_read_vpage(ftl_ctrl[i], 0) >= 0 && ftl_meta[0] < page0usn) + { + page0usn = ftl_meta[0]; + ftl_ctrl_idx = i; + } + } + ftl_cxt.usn = page0usn; + ftl_ctrl_v = sbpages; + return 0; +} + +/* phase B: classify every other superblock by its last page, or by page 0 + * when that is erased or unreadable; closed blocks go into the map, the + * higher usn winning. Returns the highest user-data usn, which phase C + * needs and which the context records. */ +static uint32_t ftl_restore_classify(void) +{ + uint32_t sb, lblock, maxusn = 0; + int type; + + memset(ftl_map, 0xff, sizeof(ftl_map)); + for (sb = 0; sb < nsuperblocks; sb++) + { + struct ftl_rs *r = &ftl_rs[sb]; + + if (r->state == RS_CTRL) + continue; + r->state = RS_FREE; + type = ftl_read_vpage(sb, sbpages - 1); + if (type < 0 || type == SPARE_ERASED) + { + type = ftl_read_vpage(sb, 0); + if (type != SPARE_DATA) + continue; + r->state = RS_LOG; + } + else if (type == SPARE_DATA_LAST) + r->state = RS_DATA; + else if (type == SPARE_DATA) + r->state = RS_LOG; + else + continue; + lblock = ftl_meta[0] / sbpages; + if (lblock >= usersb) + { + r->state = RS_FREE; + continue; + } + r->lblock = lblock; + r->usn = ftl_meta[1]; + if (r->usn > maxusn) + maxusn = r->usn; + if (r->state != RS_DATA) + continue; + if (ftl_map[lblock] != 0xffff) + { + if (ftl_rs[ftl_map[lblock]].usn >= r->usn) + { + r->state = RS_FREE; + continue; + } + ftl_rs[ftl_map[lblock]].state = RS_FREE; + } + ftl_map[lblock] = sb; + } + ftl_cxt.maxusn = maxusn; + return maxusn; +} + +/* phase C: place the logs, one per logical block */ +static int ftl_restore_place_logs(uint32_t maxusn) +{ + /* Static for the same reason as ftl_rs_slots[]: one struct ftl_log is + * about 2KB, too much to put on the stack. Phase C alone uses it. */ + static struct ftl_log cand; + uint32_t sb, i, lblock; + + for (i = 0; i < FTL_RS_SLOTS; i++) + ftl_rs_slots[i].lblock = 0xffff; + for (sb = 0; sb < nsuperblocks; sb++) + { + struct ftl_rs *r = &ftl_rs[sb]; + uint32_t datausn = 0; + + if (r->state != RS_LOG) + continue; + lblock = r->lblock; + if (ftl_map[lblock] == 0xffff) + { + if (ftl_log_in_place(sb)) + { + ftl_map[lblock] = sb; + r->state = RS_DATA; + continue; + } + } + else + datausn = ftl_rs[ftl_map[lblock]].usn; + if (r->usn < datausn) + { + r->state = RS_FREE; + continue; + } + for (i = 0; i < FTL_RS_SLOTS; i++) + { + struct ftl_log *s = &ftl_rs_slots[i]; + + if (s->lblock == 0xffff) + { + s->lblock = lblock; + s->sb = sb; + s->usn = r->usn; + break; + } + if (s->lblock != lblock) + continue; + if (r->usn != maxusn && s->usn != maxusn) + { + /* Neither was being written last: the newer one stays */ + if (s->usn >= r->usn) + r->state = RS_FREE; + else + { + ftl_rs[s->sb].state = RS_FREE; + s->sb = sb; + s->usn = r->usn; + } + } + else + { + /* One was being written last. It stays only if it holds + * every page the other does; otherwise it is a partial + * copy and the other still has the data. */ + const struct ftl_log *newer, *older; + uint32_t idx, loserusn; + + cand.sb = sb; + cand.usn = r->usn; + ftl_scan_log(&cand); + ftl_scan_log(s); + newer = r->usn == maxusn ? &cand : s; + older = newer == &cand ? s : &cand; + for (idx = 0; idx < sbpages; idx++) + if (older->offsets[idx] != FTL_NO_PAGE + && newer->offsets[idx] == FTL_NO_PAGE) + break; + loserusn = idx == sbpages ? older->usn : newer->usn; + if (loserusn == r->usn) + r->state = RS_FREE; + else + { + ftl_rs[s->sb].state = RS_FREE; + s->sb = sb; + s->usn = r->usn; + } + } + break; + } + if (i == FTL_RS_SLOTS) + return -31; + } + /* An eighteenth log: drop the one being written last - which may be the + * eighteenth itself - or fail */ + if (ftl_rs_slots[FTL_MAX_LOGS].lblock != 0xffff) + { + for (i = 0; i <= FTL_MAX_LOGS; i++) + if (ftl_rs_slots[i].usn == maxusn) + break; + if (i > FTL_MAX_LOGS) + { + logf("ftl: restore found more than %d logs", FTL_MAX_LOGS); + return -32; + } + ftl_rs[ftl_rs_slots[i].sb].state = RS_FREE; + if (i < FTL_MAX_LOGS) + ftl_rs_slots[i] = ftl_rs_slots[FTL_MAX_LOGS]; + ftl_rs_slots[FTL_MAX_LOGS].lblock = 0xffff; + } + return 0; +} + +/* phase D: a logical block with no data block gets the next free one. The + * blocks handed out here are the holes ftl_is_hole() reports; the caller + * erases them when the mount is writable (phase H). */ +static int ftl_restore_fill_holes(void) +{ + uint32_t lblock, f = 0; + + for (lblock = 0; lblock < usersb; lblock++) + { + if (ftl_map[lblock] != 0xffff) + continue; + while (f < nsuperblocks && ftl_rs[f].state != RS_FREE) + f++; + if (f == nsuperblocks) + return -33; + ftl_rs[f].state = RS_DATA; + ftl_rs[f].lblock = lblock; + ftl_map[lblock] = f; + ftl_hole[lblock / 8] |= 1 << (lblock % 8); + ftl_nholes++; + } + return 0; +} + +/* phase E: the free pool is everything left, after one slot per log */ +static int ftl_restore_build_pool(void) +{ + uint32_t sb, i, n; + + ftl_nlogs = 0; + for (i = 0; i < FTL_MAX_LOGS; i++) + if (ftl_rs_slots[i].lblock != 0xffff) + ftl_logs[ftl_nlogs++] = ftl_rs_slots[i]; + n = 0; + for (i = 0; i < ftl_nlogs; i++) + ftl_cxt.blockpool[i] = 0xffff; + for (sb = 0; sb < nsuperblocks && ftl_nlogs + n < FTL_POOL_SIZE; sb++) + if (ftl_rs[sb].state == RS_FREE) + ftl_cxt.blockpool[ftl_nlogs + n++] = sb; + ftl_cxt.freecount = n; + ftl_cxt.nextfreeidx = ftl_nlogs % FTL_POOL_SIZE; + if (ftl_nlogs + n != FTL_POOL_SIZE) + { + logf("ftl: restore found %u logs and %lu free blocks", ftl_nlogs, + (unsigned long)n); + return -34; + } + return 0; +} + +/* phase G: the newest erase counter, read counter and stats pages, walking + * the control blocks newest first and each one backwards */ +static void ftl_restore_side_pages(void) +{ + /* One flag per side-table slot. Static only to keep it off the mount's + * stack; nothing outside this phase reads it. */ + static uint32_t seen[2 * FTL_CXT_PAGELIST + 1]; + uint32_t i, k, blk, order[3], nec; + int type; + + memset(seen, 0, sizeof(seen)); + nec = (nsuperblocks * 2 + pagesize - 1) / pagesize; + for (k = 0; k < 3; k++) + order[k] = (ftl_ctrl_idx + 3 - k) % 3; + for (k = 0; k < 3; k++) + { + blk = ftl_ctrl[order[k]]; + for (i = sbpages; i-- > 0; ) + { + uint32_t idx, slot; + + type = ftl_read_vpage(blk, i); + idx = ftl_meta[1] & 0xffff; + if (type == SPARE_FTL_ERASECTR && idx < nec) + slot = idx; + else if (type == SPARE_FTL_READCOUNT && idx < nec) + slot = FTL_CXT_PAGELIST + idx; + else if (type == SPARE_FTL_STATS) + slot = 2 * FTL_CXT_PAGELIST; + else + continue; + if (seen[slot]) + continue; + seen[slot] = 1; + if (type == SPARE_FTL_ERASECTR) + ftl_cxt.ecpages[idx] = ftl_vpn(blk, i); + else if (type == SPARE_FTL_READCOUNT) + ftl_cxt.rcpages[idx] = ftl_vpn(blk, i); + else + { + ftl_cxt.statspage = ftl_vpn(blk, i); + ftl_cxt.statsflag = 0xffffffff; + } + } + } +} + +/* phase I: what each log holds */ +static void ftl_restore_scan_logs(void) +{ + uint32_t i; + + for (i = 0; i < ftl_nlogs; i++) + { + ftl_scan_log(&ftl_logs[i]); + ftl_logs[i].pagesused = sbpages; + ftl_logs[i].issequential = 0; + } +} + +static int ftl_restore(void) +{ + uint32_t maxusn; + int ret; + + logf("ftl: restoring from the medium"); + memset(ftl_rs, 0, sizeof(ftl_rs)); + memset(ftl_hole, 0, sizeof(ftl_hole)); + ftl_nholes = 0; + + ret = ftl_restore_ctrl_blocks(); /* phase A */ + if (ret) + return ret; + maxusn = ftl_restore_classify(); /* phase B */ + ret = ftl_restore_place_logs(maxusn); /* phase C */ + if (ret) + return ret; + ret = ftl_restore_fill_holes(); /* phase D */ + if (ret) + return ret; + ret = ftl_restore_build_pool(); /* phase E */ + if (ret) + return ret; + ftl_restore_side_pages(); /* phase G */ + ftl_restore_scan_logs(); /* phase I */ + ftl_cxt.cleanflag = 0; + return 0; +} + +/* Which superblock and page hold lpn: the newest log that has it, else the + * block map */ +static int ftl_resolve(uint32_t lpn, uint32_t *sb, uint32_t *v) +{ + uint32_t lblock = lpn / sbpages, idx = lpn % sbpages, i; + uint32_t bestusn = 0; + bool found = false; + + if (lblock >= usersb) + return -1; + *sb = ftl_map[lblock]; + *v = idx; + for (i = 0; i < ftl_nlogs; i++) + { + const struct ftl_log *log = &ftl_logs[i]; + if (log->lblock == lblock && log->offsets[idx] != FTL_NO_PAGE + && (!found || log->usn > bestusn)) + { + *sb = log->sb; + *v = log->offsets[idx]; + bestusn = log->usn; + found = true; + } + } + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Write path */ +/* */ +/* A write goes to a log superblock for its logical block. Small */ +/* writes append pages to the log; a write covering a whole logical */ +/* block goes straight into a fresh superblock. A log is folded back */ +/* into the block map when it fills, when its slot is needed, and at */ +/* ftl_sync(); a commit records any log still open. */ +/* */ +/* Commits are in exactly Apple's format, so its own firmware still */ +/* mounts the result. */ +/* ------------------------------------------------------------------ */ + +/* Read one of the context's pointer lists: npages pages of type, holding + * entries 16-bit table entries between them */ +static int ftl_read_side(const uint32_t *list, uint32_t npages, uint32_t type, + uint16_t *table, uint32_t entries) +{ + uint32_t per = pagesize / 2; + uint32_t i, vpn, n; + + for (i = 0; i < npages; i++) + { + vpn = list[i]; + if (vpn / sbpages >= nsuperblocks + || ftl_read_vpage(vpn / sbpages, vpn % sbpages) != (int)type + || (ftl_meta[1] & 0xffff) != i) + return -1; + n = MIN(entries - i * per, per); + memcpy(table + i * per, ftl_buffer, n * 2); + } + return 0; +} + +/* Load the rest of what a commit has to reproduce: the erase and read + * counters and the stats page. Failure is not fatal - the FTL stays mounted + * read-only. */ +static void ftl_open_write_state(void) +{ + uint32_t vpn; + + ftl_writable = false; + ftl_nmappages = (usersb * 2 + pagesize - 1) / pagesize; + ftl_necpages = (nsuperblocks * 2 + pagesize - 1) / pagesize; + ftl_nrcpages = ftl_necpages; + ftl_nlogpages = (sbpages * FTL_MAX_LOGS * 2 + pagesize - 1) / pagesize; + + if (ftl_necpages > FTL_SIDE_PAGES || ftl_nlogpages > FTL_LOG_PAGES + || nsuperblocks > FTL_SIDE_ENTRIES) + return; + + /* Lost counter tables read as zero, as the firmware's restore leaves + * them */ + if (ftl_read_side(ftl_cxt.ecpages, ftl_necpages, SPARE_FTL_ERASECTR, + ftl_erasectr, nsuperblocks)) + { + logf("ftl: erase counters lost"); + memset(ftl_erasectr, 0, sizeof(ftl_erasectr)); + } + if (ftl_read_side(ftl_cxt.rcpages, ftl_nrcpages, SPARE_FTL_READCOUNT, + ftl_readcount, nsuperblocks)) + { + logf("ftl: read counters lost"); + memset(ftl_readcount, 0, sizeof(ftl_readcount)); + } + + /* The stats page is optional; an all-0xff one is harmless */ + memset(ftl_stats, 0xff, sizeof(ftl_stats)); + vpn = ftl_cxt.statspage; + if (ftl_cxt.statsflag == 0xffffffff && vpn / sbpages < nsuperblocks + && ftl_read_vpage(vpn / sbpages, vpn % sbpages) == SPARE_FTL_STATS) + memcpy(ftl_stats, ftl_buffer, pagesize); + + /* A spare superblock is free or a log; more than 20 of them means the + * context and the medium disagree, and we stay read-only */ + if (ftl_cxt.freecount + ftl_nlogs > FTL_POOL_SIZE) + { + logf("ftl: %lu free + %u logs > %d", (unsigned long)ftl_cxt.freecount, + ftl_nlogs, FTL_POOL_SIZE); + return; + } + ftl_writable = true; +} + +/* ---- VFL write side: replacing blocks that have gone bad ----------- */ +/* Ported from ftl-nano2g.c (ftl_vfl_store_cxt, ftl_vfl_commit_cxt, + * ftl_vfl_remap_block). The Nano 3G keeps one remap table per plane rather + * than a single table plus a bad-block bitmap, and has no bbt to maintain, + * so the table update differs; everything else is the same scheme. */ + +/* Write 8 copies of a bank's VFL context; it counts as stored if at least 5 + * read back intact, as Apple's _StoreVFLCxt requires. */ +static int ftl_vfl_store_cxt(uint32_t bank) +{ + struct ftl_vfl_cxt *cxt = &ftl_vfl_cxt[bank]; + uint32_t meta[NAND_META_WORDS]; + uint32_t i, block, first, good = 0; + + first = cxt->nextcxtpage; + cxt->updatecount--; + cxt->usn = ++ftl_vfl_usn; + cxt->nextcxtpage += 8; + ftl_vfl_calculate_checksum(cxt, &cxt->checksum1, &cxt->checksum2); + + if (cxt->activecxtblock >= 4) + return -1; + block = cxt->vflcxtblocks[cxt->activecxtblock]; + if (block >= geo->blocks || cxt->nextcxtpage > geo->pagesperblock) + return -1; + + /* Eight copies in page order, as Apple's _WriteVFLCxtPage writes them: + * pages of an MLC block must be programmed in order. The first failed + * program abandons this block. */ + meta[0] = cxt->updatecount; + meta[1] = 0xffffffff; + meta[2] = 0xffff0000 | (SPARE_VFL_CXT << 8); /* byte 8 is 0 here */ + memset(ftl_cxtpage, 0xff, pagesize); + memcpy(ftl_cxtpage, cxt, sizeof(*cxt)); + for (i = 0; i < 8; i++) + if (nand_write_page(bank, block * geo->pagesperblock + first + i, + ftl_cxtpage, meta)) + return -1; + for (i = 0; i < 8; i++) + { + if (ftl_raw_read(bank, block * geo->pagesperblock + first + i) + != SPARE_VFL_CXT + || (ftl_meta[2] & 0xff) != 0 + || ftl_meta[0] != cxt->updatecount + || memcmp(ftl_buffer, cxt, offsetof(struct ftl_vfl_cxt, + checksum1))) + continue; + good++; + } + /* Apple accepts a commit with at most three bad copies */ + return good >= 5 ? 0 : -1; +} + +/* Commit a bank's VFL context, moving to another block of the context ring + * if the current one is full or will not take it. */ +static int ftl_vfl_commit_cxt(uint32_t bank) +{ + struct ftl_vfl_cxt *cxt = &ftl_vfl_cxt[bank]; + uint32_t start = cxt->activecxtblock, i, j; + + /* The mount takes the control block list from whichever bank's context + * is newest, and this commit is about to make one of them newest, so + * every bank's copy has to agree first. Same as nano2g's + * ftl_store_ctrl_block_list(). */ + for (i = 0; i < geo->banks; i++) + memcpy(ftl_vfl_cxt[i].ftlctrlblocks, ftl_ctrl, sizeof(ftl_ctrl)); + + if ((uint32_t)cxt->nextcxtpage + 8 <= geo->pagesperblock + && !ftl_vfl_store_cxt(bank)) + return 0; + + i = start; + while (1) + { + i = (i + 1) & 3; + if (i == start) + break; + if (cxt->vflcxtblocks[i] >= geo->blocks) + continue; + for (j = 0; j < 4; j++) + if (!nand_erase_block(bank, cxt->vflcxtblocks[i])) + break; + if (j == 4) + continue; + cxt->activecxtblock = i; + cxt->nextcxtpage = 0; + if (!ftl_vfl_store_cxt(bank)) + return 0; + } + /* The RAM remap table has already changed. Continuing after a failed + * commit would use mappings that cannot survive a reboot. Stop here, + * as ftl_vfl_commit_cxt() does on the Nano 2G. */ + panicf("VFL: Failed to commit VFL CXT on bank %lu!", + (unsigned long)bank); + return -1; +} + +/* Take a physical block off a context's pending-bad list */ +static void ftl_vfl_unpend(struct ftl_vfl_cxt *cxt, uint32_t block) +{ + uint32_t i, j; + + for (i = 0; i < cxt->pendingcount && i < FTL_VFL_PENDING; ) + if (cxt->pending[i] == block) + { + for (j = i; j + 1 < cxt->pendingcount && j + 1 < FTL_VFL_PENDING; + j++) + cxt->pending[j] = cxt->pending[j + 1]; + cxt->pendingcount--; + } + else + i++; +} + +/* Replace a virtual block that has gone bad with one of the VFL's reserved + * spares, and commit so the replacement survives a remount. The unit's own + * table is used first, then any other with a free slot. */ +static int ftl_vfl_remap_block(uint32_t bank, uint32_t unit, uint32_t vblock) +{ + struct ftl_vfl_cxt *cxt = &ftl_vfl_cxt[bank]; + uint32_t block = ftl_unit_block(unit, vblock); + uint32_t n = geo->planes * geo->vflspares; + uint32_t i, k, slot = n, newblock; + + for (i = 0; i < n && slot == n; i++) + { + k = (unit * geo->vflspares + i) % n; + if (cxt->remap[k] == FTL_VFL_FREE) + slot = k; + } + if (slot == n) + { + logf("ftl: bank %lu is out of spare blocks", (unsigned long)bank); + return -1; + } + newblock = ftl_vfl_spare_block(slot); + + for (i = 0; i < 9; i++) + if (!nand_erase_block(bank, newblock)) + break; + if (i == 9) + { + cxt->remap[slot] = FTL_VFL_BAD; /* the spare itself is no good */ + cxt->badcount++; + ftl_vfl_commit_cxt(bank); + return -1; + } + /* Replaced now, so no longer pending: Apple's list holds the block as + * it was addressed, found before its spare is retired below */ + ftl_vfl_unpend(cxt, ftl_vfl_phys_block(bank, unit, vblock)); + /* A spare that was already tried for this block and failed is unusable */ + for (k = 0; k < n; k++) + if (cxt->remap[k] == block) + { + cxt->remap[k] = FTL_VFL_BAD; + cxt->badcount++; + } + cxt->remap[slot] = block; + cxt->usedcount[slot / geo->vflspares]++; + logf("ftl: remapped bank %lu block %lu to spare %lu", + (unsigned long)bank, (unsigned long)block, (unsigned long)newblock); + return ftl_vfl_commit_cxt(bank); +} + +/* Erase one virtual block, retrying and then replacing it if it will not + * take an erase. nano2g tries three times before giving up on a block. A + * block on the pending-bad list, put there by Apple's firmware after a + * failed read or write, is replaced first, as the firmware's VFL erase does. */ +static int ftl_vfl_erase(uint32_t bank, uint32_t unit, uint32_t vblock) +{ + const struct ftl_vfl_cxt *cxt = &ftl_vfl_cxt[bank]; + uint32_t block = ftl_vfl_phys_block(bank, unit, vblock); + uint32_t i; + + for (i = 0; i < cxt->pendingcount && i < FTL_VFL_PENDING; i++) + if (cxt->pending[i] == block) + { + if (ftl_vfl_remap_block(bank, unit, vblock)) + return -1; + break; + } + + for (i = 0; i < 3; i++) + if (!nand_erase_block(bank, ftl_vfl_phys_block(bank, unit, vblock))) + return 0; + logf("ftl: erase of bank %lu unit %lu vblock %lu failed, remapping", + (unsigned long)bank, (unsigned long)unit, (unsigned long)vblock); + if (ftl_vfl_remap_block(bank, unit, vblock)) + return -1; + for (i = 0; i < 3; i++) + if (!nand_erase_block(bank, ftl_vfl_phys_block(bank, unit, vblock))) + return 0; + return -1; +} + +/* Erase every physical block of a superblock and count it */ +static int ftl_erase_sb(uint32_t sb) +{ + uint32_t bank, plane; + int rc; + + if (sb >= nsuperblocks) + return -1; + for (bank = 0; bank < geo->banks; bank++) + for (plane = 0; plane < geo->planes; plane++) + { + rc = ftl_vfl_erase(bank, plane, sb); + if (rc) + { + logf("ftl: erase of sb %lu bank %lu failed (%d)", + (unsigned long)sb, (unsigned long)bank, rc); + return -1; + } + } + if (ftl_erasectr[sb] != 0xffff) + ftl_erasectr[sb]++; + return 0; +} + +/* The usn a user data page is written with, as the firmware's write path + * numbers them: one counter, advanced whenever a write starts a new + * superblock, so + * every page written until the next such start shares it */ +static uint32_t ftl_data_usn(uint32_t v) +{ + if (v == 0) + ftl_cxt.maxusn++; + return ftl_cxt.maxusn; +} + +/* ---- page runs -------------------------------------------------------- */ +/* Pages are programmed in runs, as Apple's FTL does it: the callers gather + * consecutive pages of one superblock with ftl_run_add(), and ftl_run_write() + * hands them to the controller the way the firmware's multi-page VFL write + * does, so + * that the chips program in parallel and whole rows two planes at a time. */ + +/* Queue page v of superblock sb. Pages must follow on from the ones already + * queued, in the same superblock. buf must stay put until the run is + * written. */ +static void ftl_run_add(uint32_t sb, uint32_t v, const void *buf, + uint32_t word0, uint32_t word1, uint32_t type) +{ + uint32_t *meta = ftl_run_meta[ftl_run_n]; + + if (!ftl_run_n) + { + ftl_run_sb = sb; + ftl_run_v = v; + } + meta[0] = word0; + meta[1] = word1; + meta[2] = 0xffff00ff | (type << 8); + ftl_run[ftl_run_n].buf = buf; + ftl_run[ftl_run_n].meta = meta; + ftl_run_n++; +} + +/* Program n queued pages from the first'th in one driver call. With rows, + * n is a whole number of rows; they go two-plane, in the bank-major order + * the firmware's sequential write uses, when the chip allows it and no + * block of the superblock is remapped - a spare is not the other plane's + * twin. On + * failure, *failbank and *failplanes say which blocks were being + * programmed. */ +static int ftl_run_program(uint32_t first, uint32_t n, bool rows, + uint32_t *failbank, uint32_t *failplanes) +{ + static struct nand_write w[FTL_MAX_SBPAGES]; + static uint16_t vs[FTL_MAX_SBPAGES]; + uint32_t perrow = geo->banks * geo->planes; + uint32_t i, idx, bank, plane; + bool twoplane = rows && geo->twoplane; + int rc; + + for (bank = 0; twoplane && bank < geo->banks; bank++) + for (plane = 0; plane < geo->planes; plane++) + if (ftl_vfl_phys_block(bank, plane, ftl_run_sb) + != ftl_unit_block(plane, ftl_run_sb)) + twoplane = false; + + for (i = 0; i < n; i++) + { + idx = first + i; + if (twoplane) + { + /* Entry k of a row is bank k / planes, plane k % planes */ + uint32_t k = i % perrow; + + idx = first + i - k + (k % geo->planes) * geo->banks + + k / geo->planes; + } + w[i] = ftl_run[idx]; + vs[i] = ftl_run_v + idx; + ftl_vpage_phys(ftl_run_sb, vs[i], &w[i].bank, &w[i].page); + } + + *failbank = w[0].bank; + rc = nand_write_pages(w, n, twoplane, failbank); + if (rc) + { + *failplanes = 0; + for (i = 0; i < n; i++) + if (w[i].bank == *failbank) + *failplanes |= 1 << ((vs[i] / geo->banks) % geo->planes); + } + return rc; +} + +/* Program the queued run: pages up to the next row boundary on their own, + * whole rows together while more than one row remains, and the rest on + * their own, exactly as the firmware's multi-page VFL write splits it. */ +static int ftl_run_write(bool remap_on_fail) +{ + uint32_t perrow = geo->banks * geo->planes; + uint32_t n = ftl_run_n, done = 0, len, bank = 0, planes = 0, plane; + int rc = 0; + + ftl_run_n = 0; + if (!n) + return 0; + len = MIN(perrow - ftl_run_v % perrow, n); + if (len != perrow) + { + rc = ftl_run_program(0, len, false, &bank, &planes); + done = len; + } + if (!rc && n - done > perrow) + { + len = (n - done) / perrow * perrow; + rc = ftl_run_program(done, len, true, &bank, &planes); + done += len; + } + if (!rc && n > done) + rc = ftl_run_program(done, n - done, false, &bank, &planes); + if (rc) + { + /* Only a fresh merge destination can be discarded and rebuilt. + * Live logs and control blocks must retain their earlier pages. */ + if (!remap_on_fail) + panicf("FTL: Write error: %lu %lu!", + (unsigned long)ftl_run_sb, (unsigned long)ftl_run_v); + logf("ftl: write to sb %lu bank %lu failed (%d), remapping", + (unsigned long)ftl_run_sb, (unsigned long)bank, rc); + /* The blocks will not take programs any more. Replace them from the + * VFL's spares now; whatever was already written to the superblock + * is lost, so the caller has to start it again. */ + for (plane = 0; plane < geo->planes; plane++) + if (planes & (1 << plane)) + ftl_vfl_remap_block(bank, plane, ftl_run_sb); + return -1; + } + return 0; +} + +/* Program one page on its own */ +static int ftl_write_vpage(uint32_t sb, uint32_t v, const void *buf, + uint32_t word0, uint32_t word1, uint32_t type, + bool remap_on_fail) +{ + ftl_run_add(sb, v, buf, word0, word1, type); + return ftl_run_write(remap_on_fail); +} + +/* Take a block off the free ring, least-worn first. The firmware's free-block + * take picks + * the entry with the lowest erase count and swaps it to the head of the ring + * before returning it; doing the same spreads wear over the pool instead of + * cycling through it blindly, which matters because a block reclaimed from + * cold data can join the pool thousands of cycles behind the rest. */ +static int ftl_pool_alloc(uint32_t *sb, bool worn_first) +{ + uint32_t i, idx, best = 0; + uint32_t bestec = worn_first ? 0 : 0xffffffff; + bool found = false; + uint16_t t; + + if (!ftl_cxt.freecount) + return -1; + for (i = 0; i < ftl_cxt.freecount; i++) + { + uint32_t ec; + + idx = (ftl_cxt.nextfreeidx + i) % FTL_POOL_SIZE; + if (ftl_cxt.blockpool[idx] >= nsuperblocks) + continue; + ec = ftl_erasectr[ftl_cxt.blockpool[idx]]; + if (!found || (worn_first ? ec > bestec : ec < bestec)) + { + bestec = ec; + best = idx; + found = true; + } + } + if (!found) + return -1; + + idx = ftl_cxt.nextfreeidx; + t = ftl_cxt.blockpool[idx]; + ftl_cxt.blockpool[idx] = ftl_cxt.blockpool[best]; + ftl_cxt.blockpool[best] = t; + *sb = ftl_cxt.blockpool[idx]; + + /* Pool blocks are erased when released, and the whole pool again by a + * restore, as the firmware's free-block return and its restore do */ + ftl_cxt.nextfreeidx = (ftl_cxt.nextfreeidx + 1) % FTL_POOL_SIZE; + ftl_cxt.freecount--; + return 0; +} + +static int ftl_pool_release(uint32_t sb) +{ + if (ftl_cxt.freecount >= FTL_POOL_SIZE) + { + logf("ftl: releasing sb %lu with a full pool", (unsigned long)sb); + return -1; + } + if (ftl_erase_sb(sb)) + return -1; + ftl_cxt.blockpool[(ftl_cxt.nextfreeidx + ftl_cxt.freecount) + % FTL_POOL_SIZE] = sb; + ftl_cxt.freecount++; + return 0; +} + +static struct ftl_log *ftl_get_log_entry(uint32_t lblock); + +/* The current contents of lpn, for copying into a merged block. A sector + * that was never written reads as 0xff. */ +static int ftl_read_current(uint32_t lpn, uint8_t *dst) +{ + uint32_t sb, v, bank, page, meta[NAND_META_WORDS]; + int rc; + + if (ftl_resolve(lpn, &sb, &v)) + return -1; + if (ftl_nholes && sb == ftl_map[lpn / sbpages] + && ftl_is_hole(lpn / sbpages)) + { + memset(dst, 0xff, pagesize); + return 0; + } + ftl_vpage_phys(sb, v, &bank, &page); + rc = nand_read_page(bank, page, dst, meta); + if (rc < 0 || rc == NAND_ECC_FAILED) + return -1; + if (meta_type(meta) == SPARE_ERASED) + { + memset(dst, 0xff, pagesize); + return 0; + } + if (!meta_is_data(meta) || meta[0] != lpn) + { + logf("ftl: merge read lpn %lx from sb %lu v %lu: type %lx lpn %lx", + (unsigned long)lpn, (unsigned long)sb, (unsigned long)v, + (unsigned long)meta_type(meta), (unsigned long)meta[0]); + return -1; + } + return 0; +} + +/* Read one prepared part of a run. Apple retries a failed multi-page read + * page by page; keep the BootROM-derived single-page path for that retry. */ +static int ftl_read_prepared(uint32_t first, uint32_t n, + const uint32_t *lpns) +{ + uint32_t i; + int failed = nand_read_pages(ftl_read_run + first, n) != 0; + + for (i = first; i < first + n; i++) + { + struct nand_read *r = &ftl_read_run[i]; + uint32_t *meta = ftl_read_meta[i]; + + if (failed || r->ecc == NAND_ECC_FAILED) + { + r->ecc = nand_read_page(r->bank, r->page, r->buf, meta); + if (r->ecc < 0 || r->ecc == NAND_ECC_FAILED) + return -1; + } + /* Corrected data is safe to return: the controller has repaired it. + * TODO: Implement Apple's refresh list so persistently corrected + * blocks are proactively rewritten. This is media maintenance, not + * a loss of ECC protection compared with the earlier read path. */ + if (meta_type(meta) == SPARE_ERASED) + memset(r->buf, 0xff, pagesize); + else if (!meta_is_data(meta) || meta[0] != lpns[i]) + { + logf("ftl: lpn %lx: spare says %lx type %lx", + (unsigned long)lpns[i], (unsigned long)meta[0], + (unsigned long)meta_type(meta)); + return -1; + } + } + return 0; +} + +/* Read current logical pages into consecutive buffers. With a log the + * firmware uses one scattered VFL request. Without one, its multi-page VFL + * read splits at a four-bank row: an unaligned head, whole rows only while + * more than one row remains, then the final row/tail. */ +static int ftl_read_pages(uint32_t lpn, uint32_t n, uint8_t *dst) +{ + static uint32_t lpns[FTL_MAX_SBPAGES]; + uint32_t i, nr = 0, sb, v, bank, page, head, middle; + uint32_t lblock = lpn / sbpages; + bool scattered = ftl_get_log_entry(lblock) != NULL; + + if (!n || n > FTL_MAX_SBPAGES || lblock != (lpn + n - 1) / sbpages) + return -1; + for (i = 0; i < n; i++) + { + if (ftl_resolve(lpn + i, &sb, &v)) + return -1; + if (ftl_nholes && sb == ftl_map[lblock] && ftl_is_hole(lblock)) + { + memset(dst + i * pagesize, 0xff, pagesize); + continue; + } + ftl_vpage_phys(sb, v, &bank, &page); + ftl_read_run[nr].bank = bank; + ftl_read_run[nr].page = page; + ftl_read_run[nr].buf = dst + i * pagesize; + ftl_read_run[nr].meta = ftl_read_meta[nr]; + lpns[nr++] = lpn + i; + } + if (!nr) + return 0; + if (scattered) + return ftl_read_prepared(0, nr, lpns); + + v = lpn % sbpages; + head = MIN(nr, geo->banks - v % geo->banks); + if (head == geo->banks) + head = 0; + if (head && ftl_read_prepared(0, head, lpns)) + return -1; + i = head; + middle = nr - i > geo->banks + ? ((nr - i - 1) / geo->banks) * geo->banks : 0; + if (middle && ftl_read_prepared(i, middle, lpns)) + return -1; + i += middle; + return i == nr ? 0 : ftl_read_prepared(i, nr - i, lpns); +} + +/* Drop the log blocks of a logical block, returning them to the pool */ +static int ftl_release_logs(uint32_t lblock) +{ + unsigned int i = 0; + int ret = 0; + + while (i < ftl_nlogs) + { + if (ftl_logs[i].lblock != lblock) + { + i++; + continue; + } + if (ftl_pool_release(ftl_logs[i].sb)) + ret = -1; + ftl_logs[i] = ftl_logs[--ftl_nlogs]; + } + return ret; +} + +/* Does src() have new contents for every page of this logical block? Then + * the block can be written outright and no log is needed - the one case + * where there is no write amplification at all. */ +static bool ftl_block_complete(uint32_t lblock, ftl_write_src src, void *ctx) +{ + uint32_t v; + + for (v = 0; v < sbpages; v++) + if (!src(lblock * sbpages + v, ctx)) + return false; + return true; +} + +/* Write every page of a logical block into sb, taking new contents from + * src() where it has them and the block's current contents where it does + * not. New contents for the whole block go in one run, as the firmware's write + * path writes + * them; a copy goes an eighth of the block at a time, read and then + * written, as the firmware's simple merge does. */ +static int ftl_fill_block(uint32_t lblock, uint32_t sb, ftl_write_src src, + void *ctx) +{ + bool whole = src && ftl_block_complete(lblock, src, ctx); + uint32_t v, lpn, n = 0; + + for (v = 0; v < sbpages; v++) + { + const uint8_t *new_data; + + lpn = lblock * sbpages + v; + new_data = src ? src(lpn, ctx) : NULL; + if (!new_data) + { + new_data = ftl_copybuf + n * pagesize; + if (!src && n == 0 + && ftl_read_pages(lpn, MIN(sbpages - v, sbpages / 8), + ftl_copybuf)) + { + ftl_run_n = 0; + return -1; + } + if (src && ftl_read_current(lpn, ftl_copybuf + n * pagesize)) + { + ftl_run_n = 0; + return -1; + } + } + ftl_run_add(sb, v, new_data, lpn, ftl_data_usn(v), + v == sbpages - 1 ? SPARE_DATA_LAST : SPARE_DATA); + if (!whole && ++n == sbpages / 8) + { + if (ftl_run_write(true)) + return -1; + n = 0; + } + } + return ftl_run_write(true); +} + +static int ftl_merge(uint32_t lblock, ftl_write_src src, void *ctx, + bool worn_first) +{ + uint32_t sb, old; + int attempt; + + if (lblock >= usersb) + return -1; + old = ftl_map[lblock]; + + /* Up to four attempts, each in a different superblock. A block that + * refused the write has been remapped by ftl_run_write() by the time + * we get here, so the next attempt gets fresh silicon. + * ftl_commit_scattered() in ftl-nano2g.c retries the same way. */ + for (attempt = 0; attempt < 4; attempt++) + { + if (ftl_pool_alloc(&sb, worn_first)) + { + logf("ftl: free pool empty"); + return -1; + } + if (!ftl_fill_block(lblock, sb, src, ctx)) + break; + logf("ftl: lblock %lu did not take, trying another superblock", + (unsigned long)lblock); + if (ftl_pool_release(sb)) + return -1; + } + if (attempt == 4) + { + logf("ftl: lblock %lu failed in four superblocks", + (unsigned long)lblock); + return -1; + } + + /* Only now is the new block complete enough to point at */ + ftl_map[lblock] = sb; + if (ftl_release_logs(lblock) || ftl_pool_release(old)) + return -1; + return 0; +} + +/* Does src() have anything for this logical block? */ +static bool ftl_block_dirty(uint32_t lblock, ftl_write_src src, void *ctx) +{ + uint32_t v; + + for (v = 0; v < sbpages; v++) + if (src(lblock * sbpages + v, ctx)) + return true; + return false; +} + +/* ---- log blocks --------------------------------------------------- */ +/* Ported from ftl-nano2g.c, which implements the same Whimory scheme. Small + * writes append pages to a log superblock instead of rewriting the whole + * logical block; the log is folded back only when it fills, when its slot is + * needed, or at a commit. */ + +static struct ftl_log *ftl_get_log_entry(uint32_t lblock) +{ + unsigned int i; + + for (i = 0; i < ftl_nlogs; i++) + if (ftl_logs[i].lblock == lblock) + return &ftl_logs[i]; + return NULL; +} + +/* Forget a log entry without touching its superblock */ +static void ftl_drop_log(struct ftl_log *log) +{ + *log = ftl_logs[--ftl_nlogs]; +} + +/* Fold a log into a freshly allocated data block, whatever is in it. Reading + * goes through ftl_resolve(), so the log's pages win where it has them. */ +static int ftl_commit_scattered(struct ftl_log *log) +{ + /* ftl_merge() releases every log of this logical block for us */ + return ftl_merge(log->lblock, NULL, NULL, false); +} + +/* A log written strictly in order is already a valid data block apart from + * the pages nobody has written yet: fill those in from the block it + * replaces and adopt it, saving a whole block copy and one erase. */ +static int ftl_commit_sequential(struct ftl_log *log) +{ + uint32_t lblock = log->lblock, sb = log->sb, old, v, n, i; + + if (!log->issequential || log->pagescurrent != log->pagesused) + return ftl_commit_scattered(log); + for (v = log->pagesused; v < sbpages; v++) + if (log->offsets[v] != FTL_NO_PAGE) + return ftl_commit_scattered(log); + + /* An eighth of the block at a time, read and then written, as + * the firmware's copy merge does */ + for (v = log->pagesused; v < sbpages; v += n) + { + n = MIN(sbpages - v, sbpages / 8); + if (ftl_read_pages(lblock * sbpages + v, n, ftl_copybuf)) + return ftl_commit_scattered(log); + for (i = 0; i < n; i++) + { + const uint8_t *buf = ftl_copybuf + i * pagesize; + + ftl_run_add(sb, v + i, buf, lblock * sbpages + v + i, + ftl_data_usn(v + i), + v + i == sbpages - 1 ? SPARE_DATA_LAST : SPARE_DATA); + } + if (ftl_run_write(false)) + return ftl_commit_scattered(log); + for (i = 0; i < n; i++) + log->offsets[v + i] = v + i; + log->pagesused += n; + log->pagescurrent += n; + } + + old = ftl_map[lblock]; + ftl_map[lblock] = sb; + ftl_drop_log(log); + return ftl_pool_release(old); +} + +/* Copy the live pages of a log, in index order, into a fresh superblock and + * release the old one, as the firmware's move merge and + * ftl_compact_scattered() in ftl-nano2g.c do. A log that has filled up + * mostly with superseded copies of + * a few sectors then costs only its live pages, not a whole block. If power + * is cut part way, the partial copy holds fewer pages than the log it was + * copying, and the mount keeps the log. */ +static int ftl_compact_scattered(struct ftl_log *log) +{ + static uint16_t offsets[FTL_MAX_SBPAGES]; + static uint32_t lpns[FTL_COPY_PAGES]; + uint32_t sb, idx, v = 0, n = 0, lpn, bank, page, i; + + if (!log->pagescurrent) + { + sb = log->sb; + ftl_drop_log(log); + return ftl_pool_release(sb); + } + if (ftl_pool_alloc(&sb, false)) + return -1; + for (idx = 0; idx < sbpages; idx++) + { + offsets[idx] = FTL_NO_PAGE; + if (log->offsets[idx] != FTL_NO_PAGE) + { + lpn = log->lblock * sbpages + idx; + ftl_vpage_phys(log->sb, log->offsets[idx], &bank, &page); + ftl_read_run[n].bank = bank; + ftl_read_run[n].page = page; + ftl_read_run[n].buf = ftl_copybuf + n * pagesize; + ftl_read_run[n].meta = ftl_read_meta[n]; + lpns[n] = lpn; + offsets[idx] = v + n; + n++; + } + /* Up to an eighth of a block of live pages at a time, as + * the firmware's move merge gathers them */ + if (n == sbpages / 8 || (n && idx == sbpages - 1)) + { + if (ftl_read_prepared(0, n, lpns)) + break; + for (i = 0; i < n; i++) + { + const uint8_t *buf = ftl_copybuf + i * pagesize; + ftl_run_add(sb, v, buf, lpns[i], ftl_data_usn(v), + SPARE_DATA); + v++; + } + if (ftl_run_write(true)) + break; + n = 0; + } + } + if (idx < sbpages) + { + /* Fold it instead; the log itself is untouched */ + if (ftl_pool_release(sb)) + return -1; + return ftl_commit_scattered(log); + } + lpn = log->sb; /* the old superblock, to release */ + memcpy(log->offsets, offsets, sizeof(log->offsets)); + log->sb = sb; + log->pagesused = v; + log->pagescurrent = v; + log->issequential = 1; + for (idx = 0; idx < v; idx++) + if (log->offsets[idx] != idx) + log->issequential = 0; + return ftl_pool_release(lpn); +} + +/* Free a log slot, as the firmware's merge and ftl_remove_scattered_block() in + * ftl-nano2g.c do. With no entry given, fold the oldest - the one whose + * pages have been sitting unmerged longest. A given log that is at most + * half live is compacted rather than folded. */ +static int ftl_remove_scattered_block(struct ftl_log *log) +{ + unsigned int i; + + ftl_cxt.swapcounter++; + if (!log) + { + uint32_t age = 0xffffffff; + + for (i = 0; i < ftl_nlogs; i++) + if (ftl_logs[i].usn <= age) + { + age = ftl_logs[i].usn; + log = &ftl_logs[i]; + } + if (!log) + return -1; + } + else if (log->pagescurrent <= sbpages / 2) + return ftl_compact_scattered(log); + if (log->issequential) + return ftl_commit_sequential(log); + return ftl_commit_scattered(log); +} + +/* A log for this logical block, making room if need be. */ +static struct ftl_log *ftl_allocate_log_entry(uint32_t lblock) +{ + struct ftl_log *log = ftl_get_log_entry(lblock); + uint32_t sb, i; + + if (log) + goto used; + if (ftl_nlogs == FTL_MAX_LOGS && ftl_remove_scattered_block(NULL)) + return NULL; + /* Keep enough of the pool back for a fold to be possible: Apple's + * the firmware's log preparation merges a victim when only three free + * blocks remain */ + if (ftl_cxt.freecount <= 3 && ftl_remove_scattered_block(NULL)) + return NULL; + if (ftl_nlogs == FTL_MAX_LOGS || ftl_pool_alloc(&sb, false)) + return NULL; + + log = &ftl_logs[ftl_nlogs++]; + log->sb = sb; + log->lblock = lblock; + log->pagesused = 0; + log->pagescurrent = 0; + log->issequential = 1; + memset(log->offsets, 0xff, sizeof(log->offsets)); +used: + /* Its recency, for choosing a victim, as the firmware's log preparation + * keeps it */ + log->usn = ftl_cxt.maxusn - 1; + if (!log->usn) + for (i = 0; i < ftl_nlogs; i++) + ftl_logs[i].usn = 0; + return log; +} + +/* Append one page to a log, as ftl_write() in ftl-nano2g.c does. The page + * is only queued: the firmware's write path hands everything it appends to a + * log to the VFL + * in one run, and ftl_run_write() must be called before anything else + * touches the flash. */ +static void ftl_log_append(struct ftl_log *log, uint32_t idx, + const void *data) +{ + uint32_t v = log->pagesused; + uint32_t lpn = log->lblock * sbpages + idx; + uint32_t type = SPARE_DATA; + + if (v == sbpages - 1 && log->issequential) + type = SPARE_DATA_LAST; + ftl_run_add(log->sb, v, data, lpn, ftl_data_usn(v), type); + log->pagesused++; + if (log->offsets[idx] == FTL_NO_PAGE) + log->pagescurrent++; + log->offsets[idx] = v; + if (log->pagesused != log->pagescurrent || log->offsets[idx] != idx) + log->issequential = 0; +} + +/* Static wear levelling. Allocation alone only spreads wear over blocks that + * are already being written; a block holding data nobody rewrites keeps its + * low erase count for ever while the working set grinds down. When the free + * pool has worn well ahead of the least-worn mapped block, move that block's + * data into the *most* worn free one, so the cold block joins the pool. + * One relocation per call; callers ration calls with swapcounter. */ +#define FTL_WEAR_SPREAD 64 + +static int ftl_wear_level(void) +{ + uint32_t i, sb, coldlb = usersb, coldec = 0xffff, poolmax = 0; + + if (!ftl_cxt.freecount) + return 0; + for (i = 0; i < ftl_cxt.freecount; i++) + { + sb = ftl_cxt.blockpool[(ftl_cxt.nextfreeidx + i) % FTL_POOL_SIZE]; + if (sb < nsuperblocks && ftl_erasectr[sb] > poolmax) + poolmax = ftl_erasectr[sb]; + } + for (i = 0; i < usersb; i++) + if (ftl_map[i] < nsuperblocks && ftl_erasectr[ftl_map[i]] < coldec) + { + coldec = ftl_erasectr[ftl_map[i]]; + coldlb = i; + } + if (coldlb == usersb || poolmax <= coldec + FTL_WEAR_SPREAD) + return 0; + + logf("ftl: wear levelling lblock %lu (ec %lu) vs pool max %lu", + (unsigned long)coldlb, (unsigned long)coldec, + (unsigned long)poolmax); + return ftl_merge(coldlb, NULL, NULL, true); +} + +/* Reserve n consecutive pages in a control block. A full block is erased and + * reused in place, exactly as the firmware does - no pool block and + * no VFL commit is needed for this. */ +static int ftl_ctrl_reserve(uint32_t n, uint32_t *sb, uint32_t *v) +{ + if (n > sbpages) + return -1; + if (ftl_ctrl_v + n > sbpages) + { + uint32_t next = (ftl_ctrl_idx + 1) % 3; + + if (ftl_ctrl[next] >= nsuperblocks || ftl_erase_sb(ftl_ctrl[next])) + return -1; + ftl_ctrl_idx = next; + ftl_ctrl_v = 0; + } + *sb = ftl_ctrl[ftl_ctrl_idx]; + *v = ftl_ctrl_v; + ftl_ctrl_v += n; + return 0; +} + +/* Mark the context dirty before the first write after a commit, as + * the firmware's mark-context-invalid step does. A mount, ours or the + * firmware's, then finds a 0x4f page + * after the context and runs the restore, instead of trusting a context that + * predates the writes. */ +static int ftl_mark_dirty(void) +{ + uint32_t sb, v; + + if (!ftl_cxt.cleanflag) + return 0; /* already marked */ + if (ftl_ctrl_reserve(1, &sb, &v)) + return -1; + memset(ftl_buffer, 0xff, pagesize); + if (ftl_write_vpage(sb, v, ftl_buffer, ftl_cxt.usn - 1, 0xffffffff, + SPARE_FTL_MOUNT, false)) + return -1; + ftl_cxt.cleanflag = 0; + return 0; +} + +/* Write the 25-page commit: erase counters, read counters, map, log tables, + * stats, then the context, all carrying the same usn, in Apple's order, as + * the firmware's context store does. The open logs are recorded in the + * context's log slots + * and the offset tables, so a commit need not fold them. */ +static int ftl_commit(void) +{ + uint32_t per = pagesize / 2; + uint32_t npages, usn, sb, v0, i; + uint32_t ecv, rcv, mapv, logv, statsv, cxtv; + + npages = FTL_COMMIT_PAGES(ftl_necpages, ftl_nrcpages, ftl_nmappages, + ftl_nlogpages); + if (npages > sbpages || npages * pagesize > sizeof(ftl_copybuf)) + return -1; + + if (ftl_ctrl_reserve(npages, &sb, &v0)) + return -1; + usn = ftl_cxt.usn - npages; + + ecv = v0; + rcv = ecv + ftl_necpages; + mapv = rcv + ftl_nrcpages; + logv = mapv + ftl_nmappages; + statsv = logv + ftl_nlogpages; + cxtv = statsv + 1; + + /* The whole commit goes to the flash as one run, as the firmware's context + * store writes + * it; the table pages are staged in ftl_copybuf until then */ + for (i = 0; i < ftl_necpages; i++) + { + uint8_t *buf = ftl_copybuf + (ecv - v0 + i) * pagesize; + + memset(buf, 0xff, pagesize); + memcpy(buf, ftl_erasectr + i * per, + MIN(nsuperblocks - i * per, per) * 2); + ftl_run_add(sb, ecv + i, buf, usn, 0xffff0000 | i, + SPARE_FTL_ERASECTR); + ftl_cxt.ecpages[i] = ftl_vpn(sb, ecv + i); + } + for (i = 0; i < ftl_nrcpages; i++) + { + uint8_t *buf = ftl_copybuf + (rcv - v0 + i) * pagesize; + + memset(buf, 0xff, pagesize); + memcpy(buf, ftl_readcount + i * per, + MIN(nsuperblocks - i * per, per) * 2); + ftl_run_add(sb, rcv + i, buf, usn, 0xffff0000 | i, + SPARE_FTL_READCOUNT); + ftl_cxt.rcpages[i] = ftl_vpn(sb, rcv + i); + } + for (i = 0; i < ftl_nmappages; i++) + { + uint8_t *buf = ftl_copybuf + (mapv - v0 + i) * pagesize; + + memset(buf, 0xff, pagesize); + memcpy(buf, ftl_map + i * per, MIN(usersb - i * per, per) * 2); + ftl_run_add(sb, mapv + i, buf, usn, 0xffff0000 | i, SPARE_FTL_MAP); + ftl_cxt.mappages[i] = ftl_vpn(sb, mapv + i); + } + /* One offset table per log slot, in slot order */ + memset(ftl_logtable, 0xff, sizeof(ftl_logtable)); + for (i = 0; i < ftl_nlogs; i++) + memcpy(ftl_logtable + i * sbpages, ftl_logs[i].offsets, sbpages * 2); + for (i = 0; i < ftl_nlogpages; i++) + { + ftl_run_add(sb, logv + i, ftl_logtable + i * per, usn, 0xffff0000 | i, + SPARE_FTL_LOG); + ftl_cxt.logpages[i] = ftl_vpn(sb, logv + i); + } + ftl_run_add(sb, statsv, ftl_stats, usn, 0xffff0000, SPARE_FTL_STATS); + ftl_cxt.statspage = ftl_vpn(sb, statsv); + ftl_cxt.statsflag = 0xffffffff; + ftl_cxt.cleanflag = 1; /* everything is in the map again */ + + /* The context itself */ + ftl_cxt.usn = usn; + for (i = 0; i < 3; i++) + ftl_cxt.ctrlblocks[i] = ftl_ctrl[i]; + ftl_cxt.ctrlpage = ftl_vpn(sb, cxtv); + for (i = 0; i < FTL_CXT_LOGS; i++) + { + struct ftl_cxt_log *c = &ftl_cxt.logs[i]; + + if (i < ftl_nlogs && i < FTL_MAX_LOGS) + { + c->usn = ftl_logs[i].usn; + c->sb = ftl_logs[i].sb; + c->lblock = ftl_logs[i].lblock; + c->pagesused = ftl_logs[i].pagesused; + c->pagescurrent = ftl_logs[i].pagescurrent; + c->issequential = ftl_logs[i].issequential; + } + else + { + c->sb = 0xffff; + c->lblock = 0xffff; + } + } + memset(ftl_cxtpage, 0xff, pagesize); + memcpy(ftl_cxtpage, &ftl_cxt, sizeof(ftl_cxt)); + ftl_run_add(sb, cxtv, ftl_cxtpage, usn, 0xffffffff, SPARE_FTL_CXT); + if (ftl_run_write(false)) + return -1; + + ftl_cxt.usn = usn; + return 0; +} + +/* src for a plain contiguous write of sectors. On pages larger than a + * sector, a page the write covers only part of comes from part, holding + * the page's current contents with the new sectors laid over them. */ +struct ftl_range +{ + uint32_t first; /* sectors */ + uint32_t count; + const uint8_t *buf; + uint32_t partlpn; /* the page in part, or 0xffffffff */ + const uint8_t *part; +}; + +static const void *ftl_range_src(uint32_t lpn, void *ctx) +{ + const struct ftl_range *r = ctx; + uint64_t sector = (uint64_t)lpn * secperpage; + + if (lpn == r->partlpn) + return r->part; + if (sector < r->first || sector - r->first >= r->count) + return NULL; + return r->buf + (size_t)(sector - r->first) * NAND_PAGE_SIZE; +} + +/* The single failure path of the write side. Every failure below leaves the + * medium itself consistent - the committed map still points at whatever was + * there - but our pool or log accounting may no longer match it, so the + * mount drops to read-only and the caller is told which step failed. + * Returns the code, so a failure site is one statement. */ +static int ftl_write_fail(int code) +{ + ftl_writable = false; + return code; +} + +/* One logical block of ftl_write_merge(): mark the context dirty, then + * either rewrite the block whole or append the changed pages to a log. */ +static int ftl_write_block(uint32_t lblock, ftl_write_src src, void *ctx) +{ + struct ftl_log *log; + uint32_t idx; + int ret = 0; + + /* Anything we leave in a log block is invisible to the committed map, + * so the context has to say so before a page is written. Once per block + * rather than once per call: the commit below leaves the context clean + * again, and the blocks after it need marking too. */ + if (ftl_mark_dirty()) + return ftl_write_fail(-5); + + /* A whole block at once needs no log and costs no amplification */ + if (ftl_block_complete(lblock, src, ctx)) + { + if (ftl_merge(lblock, src, ctx, false)) + return ftl_write_fail(-3); + return 0; + } + + log = ftl_allocate_log_entry(lblock); + if (!log) + return ftl_write_fail(-6); + for (idx = 0; idx < sbpages && !ret; idx++) + { + const void *data = src(lblock * sbpages + idx, ctx); + + if (!data) + continue; + if (log->pagesused == sbpages) + { + /* Full: fold it away and start another. These two cannot + * return straight away - the run queued above still has to be + * flushed below before we leave. */ + ftl_run_write(false); + if (ftl_remove_scattered_block(log)) + { + ret = ftl_write_fail(-7); + break; + } + log = ftl_allocate_log_entry(lblock); + if (!log) + { + ret = ftl_write_fail(-6); + break; + } + } + ftl_log_append(log, idx, data); + } + /* A failed program panics: the log's earlier pages cannot be + * rebuilt, as for a single page in ftl-nano2g.c */ + ftl_run_write(false); + if (ret) + return ret; + if (log->pagesused == sbpages && ftl_remove_scattered_block(log)) + return ftl_write_fail(-7); + return 0; +} + +/* Write the pages src() has new contents for, in every logical block + * between first and last. Blocks it has nothing for are left alone. */ +static int ftl_write_merge(uint32_t first, uint32_t last, ftl_write_src src, + void *ctx) +{ + uint32_t lblock; + int ret = 0; + + if (!ftl_mounted || !ftl_writable || !src) + return -1; + if (last < first || last >= usersb * sbpages) + return -2; + + mutex_lock(&ftl_mtx); + for (lblock = first / sbpages; lblock <= last / sbpages; lblock++) + { + if (!ftl_block_dirty(lblock, src, ctx)) + continue; + ret = ftl_write_block(lblock, src, ctx); + if (ret) + break; + } + /* Static wear levelling on the budget the firmware's write path and + * ftl-nano2g.c's ftl_write() use: every merge adds one, and at 300 one + * cold block is moved. There is no commit here - like Apple's firmware, + * ftl_sync() alone commits, and an unclean mount recovers the rest. */ + if (!ret && ftl_cxt.swapcounter >= 300) + { + ftl_cxt.swapcounter -= 20; + if (ftl_wear_level()) + ret = ftl_write_fail(-4); + } + mutex_unlock(&ftl_mtx); + return ret; +} + +/* Write sectors that all lie in the page lpn and do not fill it: the rest + * of the page is read back and written with them */ +static int ftl_write_part(uint32_t lpn, uint32_t sector, uint32_t count, + const uint8_t *buf) +{ + struct ftl_range r = { sector, count, buf, lpn, ftl_sectorpage }; + int rc; + + mutex_lock(&ftl_mtx); + rc = ftl_read_pages(lpn, 1, ftl_sectorpage); + mutex_unlock(&ftl_mtx); + if (rc) + return -3; + memcpy(ftl_sectorpage + (sector - lpn * secperpage) * NAND_PAGE_SIZE, + buf, count * NAND_PAGE_SIZE); + return ftl_write_merge(lpn, lpn, ftl_range_src, &r); +} + +int ftl_write(uint32_t sector, uint32_t count, const void *buffer) +{ + struct ftl_range r = { sector, count, buffer, 0xffffffff, NULL }; + const uint8_t *buf = buffer; + uint32_t n, first, last; + int rc; + + if (!count) + return 0; + if (sector + count > ftl_num_sectors() || sector + count < sector) + return -2; + + /* A page only partly covered at either end, then the whole pages */ + if (sector % secperpage) + { + n = MIN(count, secperpage - sector % secperpage); + rc = ftl_write_part(sector / secperpage, sector, n, buf); + if (rc) + return rc; + sector += n; + count -= n; + buf += n * NAND_PAGE_SIZE; + } + if (count % secperpage) + { + n = count % secperpage; + rc = ftl_write_part((sector + count - n) / secperpage, + sector + count - n, n, + buf + (count - n) * NAND_PAGE_SIZE); + if (rc) + return rc; + count -= n; + } + if (!count) + return 0; + first = sector / secperpage; + last = (sector + count) / secperpage - 1; + r.first = sector; + r.count = count; + r.buf = buf; + return ftl_write_merge(first, last, ftl_range_src, &r); +} + +/* The commit sequence, with ftl_mtx already held. */ +static int ftl_sync_locked(void) +{ + int ret = 0; + + /* Fold every log block away - the ones we filled this session and any + * left by whoever wrote the NAND last - so the commit can say there are + * none and the free pool is whole again, as the firmware's full flush and + * ftl-nano2g.c's ftl_sync() do. A log that was written in order is + * adopted outright rather than copied. */ + if (ftl_cxt.swapcounter >= 20) + { + ftl_cxt.swapcounter -= 20; + if (ftl_wear_level()) + { + ftl_writable = false; + return -6; + } + } + while (ftl_nlogs) + { + ftl_cxt.swapcounter++; + if (ftl_commit_sequential(&ftl_logs[0])) + { + ftl_writable = false; + return -2; + } + } + if (ftl_commit()) + { + ftl_writable = false; + ret = -4; + } + return ret; +} + +int ftl_sync(void) +{ + int ret; + + if (!ftl_mounted) + return -1; + /* Nothing written since the last commit: nothing to do. Shutdown calls + * this every time, and ftl-nano2g.c returns early the same way. A + * read-only mount cannot have written anything either, as with the + * Nano 2G's FTL_READONLY. */ + if (ftl_cxt.cleanflag || ftl_readonly) + return 0; + if (!ftl_writable) + return -1; + mutex_lock(&ftl_mtx); + ret = ftl_sync_locked(); + mutex_unlock(&ftl_mtx); + return ret; +} + +int ftl_read(uint32_t sector, uint32_t count, void *buffer) +{ + uint8_t *buf = buffer; + uint32_t n, lpn, off; + int ret = 0; + + if (!ftl_mounted) + return -1; + if (!count) + return 0; + if (sector + count > ftl_num_sectors() || sector + count < sector) + return -2; + + mutex_lock(&ftl_mtx); + while (count) + { + lpn = sector / secperpage; + off = sector % secperpage; + if (off || count < secperpage) + { + /* Part of a page, through a page-sized buffer */ + n = MIN(count, secperpage - off); + if (ftl_read_pages(lpn, 1, ftl_sectorpage)) + { + ret = -3; + break; + } + memcpy(buf, ftl_sectorpage + off * NAND_PAGE_SIZE, + n * NAND_PAGE_SIZE); + } + else + { + n = MIN(count / secperpage, sbpages - lpn % sbpages); + if (ftl_read_pages(lpn, n, buf)) + { + ret = -3; + break; + } + n *= secperpage; + } + sector += n; + count -= n; + buf += n * NAND_PAGE_SIZE; + } + mutex_unlock(&ftl_mtx); + return ret; +} + +bool ftl_readonly_mount(void) +{ + /* Anything but a mounted, writable FTL takes no writes: a predicate + * that guards them answers the safe way when nothing is mounted. */ + return !ftl_mounted || !ftl_writable; +} + +uint32_t ftl_num_sectors(void) +{ + return ftl_mounted ? usersb * sbpages * secperpage : 0; +} + +/* The geometry the mount works in, from the chip table row */ +static int ftl_setup_geometry(void) +{ + geo = nand_get_geometry(); + if (!geo || geo->banks == 0 || geo->banks > FTL_MAX_BANKS) + return -1; + if (geo->layout < NAND_LAYOUT_SINGLE || geo->layout > NAND_LAYOUT_SPLIT13 + || geo->planes < 1 || geo->planes > 4 + || geo->pagesize < NAND_PAGE_SIZE + || geo->pagesize > NAND_MAX_PAGE_SIZE + || geo->pagesize % NAND_PAGE_SIZE) + return -4; + pagesize = geo->pagesize; + secperpage = pagesize / NAND_PAGE_SIZE; + /* Apple's VFL reserves each unit's top vflspares virtual blocks; below + * them are the user blocks, 3 FTL control blocks and a pool of 20 */ + nsuperblocks = geo->blocks / geo->planes - geo->vflspares; + sbpages = geo->pagesperblock * geo->planes * geo->banks; + usersb = geo->userblocks / geo->planes; + if (nsuperblocks > FTL_MAX_SB || usersb > FTL_MAX_USERSB + || usersb > nsuperblocks || sbpages > FTL_MAX_SBPAGES + || sbpages * pagesize > FTL_MAX_SBPAGES * NAND_PAGE_SIZE + || geo->planes * geo->vflspares > 820) + return -2; + return 0; +} + +int ftl_init(void) +{ + uint32_t bank; + bool restored; + int rc; + + ftl_mounted = false; + mutex_init(&ftl_mtx); + ftl_nholes = 0; + + rc = ftl_setup_geometry(); + if (rc) + return rc; + + for (bank = 0; bank < geo->banks; bank++) + { + if (ftl_vfl_open_bank(bank)) + { + logf("ftl: no VFL context on bank %lu", (unsigned long)bank); + return -3; + } + if (ftl_vfl_check_cxt(&ftl_vfl_cxt[bank])) + { + logf("ftl: VFL context on bank %lu does not fit the chip", + (unsigned long)bank); + return -5; + } + } + + for (bank = 0; bank < geo->banks; bank++) + if (ftl_vfl_cxt[bank].usn > ftl_vfl_usn) + ftl_vfl_usn = ftl_vfl_cxt[bank].usn; + + /* Apple's FTL_Open: the context if it describes the medium, else + * the restore */ + rc = ftl_load_cxt(); + if (rc < 0) + { + logf("ftl: no FTL context (%d)", rc); + return -10 + rc; + } + restored = !rc; + rc = restored ? ftl_restore() : ftl_load_clean(); + if (rc) + { + logf("ftl: %s failed (%d)", restored ? "restore" : "load", rc); + return -20 + rc; + } + ftl_open_write_state(); +#ifdef BOOTLOADER + /* Bootloaders don't need write access, as on the Nano 2G */ + ftl_writable = false; +#endif + /* A chip whose row has not been proven on hardware is only read */ + if (!geo->validated) + ftl_writable = false; + ftl_readonly = !ftl_writable; + + /* The rest of the restore, which writes: erase the blocks given to + * holes (phase D) and the free pool (H), then commit (J) */ + if (restored && ftl_writable) + { + uint32_t i, lblock; + + for (lblock = 0; lblock < usersb && !rc; lblock++) + if (ftl_is_hole(lblock)) + rc = ftl_erase_sb(ftl_map[lblock]); + ftl_nholes = 0; + for (i = 0; i < ftl_cxt.freecount && !rc; i++) + rc = ftl_erase_sb(ftl_cxt.blockpool[(ftl_cxt.nextfreeidx + i) + % FTL_POOL_SIZE]); + if (!rc) + rc = ftl_commit(); + if (rc) + { + logf("ftl: restore could not be written (%d)", rc); + ftl_writable = false; + } + } + + ftl_mounted = true; + return 0; +} diff --git a/firmware/target/arm/s5l8702/ipodnano3g/ftl-target.h b/firmware/target/arm/s5l8702/ipodnano3g/ftl-target.h new file mode 100644 index 0000000000..ac98c4cc25 --- /dev/null +++ b/firmware/target/arm/s5l8702/ipodnano3g/ftl-target.h @@ -0,0 +1,60 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2009 by Michael Sparmann + * + * Interface modelled on + * firmware/target/arm/s5l8700/ipodnano2g/ftl-target.h. + * + * Copyright (C) 2026 by Andrew Rice + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef __FTL_TARGET_H__ +#define __FTL_TARGET_H__ + +#include +#include + +/* Mount Apple's FTL. nand_init() must have succeeded. Returns 0, or a + * negative number saying which stage failed: -4 if the chip's layout or + * page size is not supported, -5 if its VFL context does not fit the chip + * table row. The FTL is mounted read-only, so that writes fail and + * ftl_sync() does nothing, in the bootloader, on a chip whose row is not + * validated, and if anything a commit must reproduce could not be + * loaded. */ +int ftl_init(void); + +/* Read count logical sectors of NAND_PAGE_SIZE bytes, one or two to a NAND + * page. Returns 0 or a negative number. */ +int ftl_read(uint32_t sector, uint32_t count, void *buffer); + +/* Write count logical sectors of NAND_PAGE_SIZE bytes. The data is on the + * medium when this returns, but the block map that finds it is not: call + * ftl_sync() to commit that. Returns 0 or a negative number. */ +int ftl_write(uint32_t sector, uint32_t count, const void *buffer); + +/* Commit the block map, so the writes above survive a remount. Returns 0 or + * a negative number; after a failure the FTL refuses further writes. */ +int ftl_sync(void); + +/* Number of logical sectors, 0 if the FTL is not mounted */ +uint32_t ftl_num_sectors(void); + +/* Whether the mounted FTL refuses writes */ +bool ftl_readonly_mount(void); + +#endif /* __FTL_TARGET_H__ */ diff --git a/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c index 3632955dc2..b4d48fad6a 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c @@ -7,6 +7,7 @@ * \/ \/ \/ \/ \/ * * Copyright (C) 2009 by Michael Sparmann + * Copyright (C) 2026 by Andrew Rice * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -18,13 +19,1042 @@ * ****************************************************************************/ +/* + * NAND driver for the iPod Nano 3G (S5L8702 FMC controller). + * + * The register sequences were worked out from the BootROM and the retail + * firmware of the owner's own player: resetting a bank, waiting for it to + * go ready, and reading a page with its per-chunk ECC. nand_identify() + * follows wInd3x (freemyipod/wInd3x, pkg/exploit/wind3x_n3g.go). + * + * Limitations: + * - Only the first FMC controller is used. Banks (chip enables) 0 to + * NAND_MAX_BANKS-1 are probed at init; banks above 0 are reachable only + * because gpio_preinit() muxes all of PCON(9), which the BootROM does not. + * - Only chips validated on hardware are driven; nand_init() refuses any + * other chip (see nand_chip_table). + * - Storage goes through Apple's FTL (ftl-nano3g.c). + */ + +#include "config.h" +#include "cpu.h" +#include "system.h" +#include "kernel.h" #include "mv.h" #include "storage.h" +#include "clocking-s5l8702.h" +#include "logf.h" +#include "panic.h" +#include "nand-target.h" +#include "ftl-target.h" +#include +#include + +#define NAND_CMD_READ 0x00 +#define NAND_CMD_READ2 0x30 +#define NAND_CMD_READSTATUS 0x70 +#define NAND_CMD_READID 0x90 +#define NAND_CMD_RESET 0xFF +#define NAND_CMD_PROGRAM 0x80 +#define NAND_CMD_PROGRAM_PLANE1 0x81 /* plane 1 of a two-plane program */ +#define NAND_CMD_PROGRAM_QUEUE 0x11 /* plane 0 queued, plane 1 to come */ +#define NAND_CMD_PROGRAM2 0x10 +#define NAND_CMD_ERASE 0x60 +#define NAND_CMD_ERASE2 0xD0 + +/* READ STATUS result */ +#define NAND_STATUS_FAIL (1 << 0) +#define NAND_STATUS_READY (1 << 6) + +/* The controller moves a page in units of 2KiB, each of four 512-byte + * chunks: one unit on 2KiB-page chips, two on 4KiB-page chips */ +#define NAND_UNIT_SIZE 2048 +#define NAND_CHUNK_SIZE 512 + +/* Bus timing: Apple's values, giving FMCTRL0 = 0x43803 for bank 0. Every + * legal combination reads the ID correctly, so these are not critical. */ +#define NAND_TUNK1 4 /* 3-bit field at bit 16 */ +#define NAND_TWP 3 /* 3-bit field at bit 12 */ + +/* Apple's poll helper (0x20000e50) gives up after 100000 reads. */ +#define NAND_POLL_SPINS 1000000 + +struct nand_chip_info +{ + uint32_t id; /* first four ID bytes, little endian */ + uint8_t banks; /* chip enables holding this part */ + uint8_t mode; /* Apple's layout and programming mode */ + uint16_t blocks; /* per chip enable */ + uint16_t pagesperblock; + uint16_t pagesize; /* bytes of data per page */ + uint16_t userblocks; /* per chip enable, as Whimory counts them */ + bool validated; /* read, written and remounted on hardware */ +}; + +/* Apple's own chip table in the Nano 3G firmware (osos 1.1.3: 18 rows of 44 + * bytes holding the id, chip enables, blocks per enable, pages per block, + * 512-byte sectors per page, spare bytes, timings, user blocks and mode). + * Apple picks the row matching both the id and the number of chip enables + * answering with it, and so does nand_init(): the same part can come in + * a different mode with a different bank count. + * + * The mode decides how the VFL groups physical blocks (nand_mode_planes() + * and nand_mode_layout()) and which program writes two planes at once. Only + * validated rows are driven; nand_init() refuses any other chip. */ +static const struct nand_chip_info nand_chip_table[] = +{ + /* Hynix: the 4GB unit this port was developed on, and the 8GB one */ + { 0xA514D3AD, 4, 8, 4096, 128, 2048, 3872, true }, + { 0xA555D5AD, 4, 8, 8192, 128, 2048, 7744, true }, +}; +/* Physical blocks per bank in one VFL block: the original firmware's mode + * switch uses 1 for mode 1, 4 for mode 4, and 2 for every other mode */ +static unsigned int nand_mode_planes(unsigned int mode) +{ + return mode == 1 ? 1 : mode == 4 ? 4 : 2; +} + +/* How the planes of a VFL block are placed: a partner in the next block, + * the other half of the chip, or both */ +static unsigned int nand_mode_layout(unsigned int mode) +{ + switch (mode) + { + case 3: case 8: return NAND_LAYOUT_ADJACENT; + case 2: case 9: case 12: return NAND_LAYOUT_HALVES; + case 4: return NAND_LAYOUT_BOTH; + case 1: return NAND_LAYOUT_SINGLE; + case 13: return NAND_LAYOUT_SPLIT13; + default: return NAND_LAYOUT_UNKNOWN; + } +} + +static const struct nand_chip_info *nand_chip; +static uint32_t nand_id; /* what bank 0 answered READ ID with */ +static unsigned int nand_banks; +static bool nand_ready = false; +static long nand_last_activity_value = -1; +static struct nand_geometry nand_geo; + +const struct nand_geometry *nand_get_geometry(void) +{ + if (!nand_ready) + return NULL; + nand_geo.banks = nand_banks; + nand_geo.blocks = nand_chip->blocks; + nand_geo.pagesperblock = nand_chip->pagesperblock; + nand_geo.planes = nand_mode_planes(nand_chip->mode); + nand_geo.userblocks = nand_chip->userblocks; + /* Whimory reserves each bank's blocks beyond its user blocks, less 23 + * superblocks: 3 FTL control blocks and a pool of 20. 89 was measured + * on the 4GB unit; the rest follow the same rule. */ + nand_geo.vflspares = (nand_chip->blocks - nand_chip->userblocks) + / nand_geo.planes - 23; + /* Mode 8 is the mode whose two-plane program nand_write_pages() + * reproduces */ + nand_geo.twoplane = nand_chip->mode == 8; + nand_geo.pagesize = nand_chip->pagesize; + nand_geo.mode = nand_chip->mode; + nand_geo.layout = nand_mode_layout(nand_chip->mode); + nand_geo.validated = nand_chip->validated; +#ifdef NAND_WRITABLE_ID + /* Validation builds for testers: mount chips with this id writable, + * whatever their bank count, e.g. -DNAND_WRITABLE_ID=0xA555D5AD */ + if (nand_chip->id == NAND_WRITABLE_ID) + nand_geo.validated = true; +#endif +#ifdef NAND_TEST_READONLY + /* Test builds: behave as on a chip that is not validated */ + nand_geo.validated = false; +#endif + return &nand_geo; +} + +/* ECC state gathered over the four chunks of a page, as the firmware's + * page read does */ +struct nand_ecc_acc +{ + bool flagged; /* FMCSTAT_UNK27 was set on some chunk */ + uint32_t unk810; /* FMUNK810 after each correction, ORed */ + uint32_t transstat; /* FMTRANSSTAT after each correction, ORed */ +}; + +static void nand_touch(void) +{ + nand_last_activity_value = current_tick; +} + +static void nand_set_fmctrl0(uint32_t bank) +{ + FMCTRL0 = (NAND_TUNK1 << 16) | (NAND_TWP << 12) | FMCTRL0_UNK1 + | 1 | FMCTRL0_CE(bank); +} + +/* Spin until bit is set in reg, then write 1 to clear it. Tight and + * bounded, like Apple's poll helper: some of these flags assert briefly. */ +static int nand_wait_reg(volatile uint32_t *reg, uint32_t bit) +{ + unsigned long spins = NAND_POLL_SPINS; + while (spins--) + { + if (*reg & bit) + { + *reg = bit; + return 0; + } + } + return -1; +} + +static int nand_wait_stat(uint32_t bit) +{ + return nand_wait_reg(&FMCSTAT, bit); +} + +/* As nand_wait_stat(), but leaves the bit set */ +static int nand_wait_stat_noclear(uint32_t bit) +{ + unsigned long spins = NAND_POLL_SPINS; + while (spins--) + if (FMCSTAT & bit) + return 0; + return -1; +} + +/* Clear every FMCSTAT bit. Needed after nand_identify() and after each page + * read: stale bits let the stage waits in nand_transfer_chunk() pass before + * the data has moved, which corrupts the next read. The original + * firmware's page read ends the same way. */ +static void nand_clear_status(void) +{ + FMCSTAT = 0xffffffff; +} + +static int nand_send_cmd(uint32_t cmd) +{ + FMCMD = cmd; + return nand_wait_stat(FMCSTAT_CMDDONE); +} + +static int nand_send_addr_byte(uint32_t addr) +{ + FMANUM = 0; + FMADDR0 = addr; + FMCTRL1 = FMCTRL1_DOTRANSADDR; + return nand_wait_stat(FMCSTAT_ADDRDONE); +} + +static int nand_send_addr_page(uint32_t page) +{ + FMANUM = 4; + FMADDR0 = page << 16; + FMADDR1 = (page >> 16) & 0xFF; + FMCTRL1 = FMCTRL1_DOTRANSADDR; + return nand_wait_stat(FMCSTAT_ADDRDONE); +} + +/* Switch the NAND pins to their NAND function: Apple's 0x20001830 with + * argument 0 (first controller). It leaves the other PCON(9) pins alone. */ +static void nand_gpio_config(void) +{ + PCON(10) = (PCON(10) & 0xFFFF0000) | 0x00002222; + PCON(9) = (PCON(9) & ~0x000F000F) | 0x00020002; + PCON(8) = 0x22222222; +} + +/* Wait for the chip to go ready, as the original firmware does after every + * operation: READ STATUS, then FMCTRL1 = 0xca has the controller read + * the status byte into FMSYND0. Apple writes FMADDR2 = 1 whatever the bank. + * Returns the status byte, or -1 on a timeout. It leaves the chip in status + * output mode, so a reader must re-issue READ. */ +static int nand_read_status(void) +{ + /* The microsecond timer, not current_tick: this runs at nand_init() + * and must be bounded with interrupts off too */ + unsigned long timeout = USEC_TIMER + 200000; + unsigned long spins; + uint32_t status = 0; + + FMADDR2 = 1; + if (nand_send_cmd(NAND_CMD_READSTATUS)) + return -1; + do + { + FMCTRL1 = 0xca; + for (spins = 10000; spins; spins--) + { + status = FMSYND0; + if (status & NAND_STATUS_READY) + break; + } + } while (!(status & NAND_STATUS_READY) + && TIME_BEFORE(USEC_TIMER, timeout)); + if (!(status & NAND_STATUS_READY)) + return -1; + FMCSTAT = FMCSTAT_STATUSREADY; + FMCTRL1 = 0x20; + return status & 0xff; +} + +/* Wait for a read or reset to finish. This used to follow Apple's ROM + * (0x20009614): READ STATUS, FMCTRL1 = 0x2a, then wait for FMCSTAT bit 23. + * That bit does set (128 reads of 128, measured), but nothing in it says + * the chip is ready; nand_read_status() checks the ready bit, as the retail + * firmware does. Both take ~300 us per page read. */ +static int nand_wait_ready(void) +{ + int status = nand_read_status(); + + FMADDR2 = 0; + FMCTRL1 = 0xe0; + return status < 0 ? NAND_ERR_READY_CMD : 0; +} + +int nand_reset(uint32_t bank) +{ + clockgate_enable(CLOCKGATE_NAND, true); + clockgate_enable(CLOCKGATE_NANDECC, true); + nand_gpio_config(); + + nand_set_fmctrl0(bank); + if (nand_send_cmd(NAND_CMD_RESET)) + return NAND_ERR_RESET_CMD; + + /* Wait out tRST as well: the old ready wait could not tell when the + * chip had left reset. */ + udelay(5000); + return nand_wait_ready(); +} + +/* Read the first four ID bytes of the bank selected by the last + * nand_reset(), following wInd3x. ext, if not NULL, gets + * the next four of the eight the controller is asked for. */ +int nand_identify(uint32_t *id, uint32_t *ext) +{ + if (nand_send_cmd(NAND_CMD_READID)) + return -1; + if (nand_send_addr_byte(0)) + return -1; + + FMDNUM = 7; + FMADDR2 = 1; + FMCSTAT = FMCSTAT_TRANSDONE; /* nand_wait_ready() leaves it set */ + FMCTRL1 = FMCTRL1_DOREADDATA | FMCTRL1_CLEARWFIFO | FMCTRL1_CLEARRFIFO + | (1 << 8); + /* wInd3x leaves TRANSDONE set here; nand_clear_status() clears it */ + if (nand_wait_stat_noclear(FMCSTAT_TRANSDONE)) + return -1; + FMADDR2 = 0x100; + FMCTRL1 = FMCTRL1_CLEARWFIFO | (1 << 8) | (1 << 9); + + *id = FMFIFO; + if (ext) + *ext = FMFIFO; + nand_clear_status(); + return 0; +} + +/* Kick the controller's correction of the chunk just read and wait for it. + * The register writes Apple's 0x2000ac1c makes, in the same order. */ +static int nand_kick_chunk(uint32_t half, uint32_t sub, uint32_t upper) +{ + FMTRANSSTAT = 0x7f; + FMTRANS1 = 0x01000180; + FMTRANS0 = (1 << (sub + 8)) | (upper << 16) | (1 << (half + 8)) | 1; + return nand_wait_reg(&FMTRANSSTAT, 1 << 2); +} + +/* Move one 512-byte quarter (chunk 0..3) of the current page to dst, as + * the firmware's page read does. Stages A and B read the chunk into the + * controller, + * which sets FMCSTAT_UNK27 if it finds ECC errors and corrects them when + * kicked. Stage C then has the controller write the chunk to dst. */ +static int nand_transfer_chunk(uint32_t chunk, void *dst, + struct nand_ecc_acc *ecc) +{ + uint32_t half = chunk & 1; + uint32_t upper = (chunk >> 1) & 1; + uint32_t sub = half + 4; + + /* Stage A: 16 units */ + FMADDR6 = upper ? 0x10 : 0; + FMADDR2 = 1 << sub; + FMDNUM = 0xf; + FMCTRL1 = 0x32; + if (nand_wait_stat(FMCSTAT_TRANSDONE)) + return -1; + + /* Stage B: 512 units */ + FMDNUM = 0x1ff; + FMADDR2 = 1 << half; + FMADDR6 = 0; + FMCTRL1 = 0x22; + if (nand_wait_stat(FMCSTAT_TRANSDONE)) + return -1; + if (FMCSTAT & FMCSTAT_UNK27) + { + if (nand_kick_chunk(half, sub, upper)) + return -1; + ecc->flagged = true; + ecc->unk810 |= FMUNK810; + ecc->transstat |= FMTRANSSTAT; + } + + /* Stage C: the controller writes dst behind the cache's back */ + commit_discard_dcache_range(dst, NAND_CHUNK_SIZE); + FMDATAW0 = (uint32_t)(intptr_t)dst; + FMDATAW1 = 7; + FMCTRL0 = (FMCTRL0 & ~FMCTRL0_ENABLEDMA) | FMCTRL0_AUTOXFER | 1; + FMADDR2 = 1 << (half + 8); + FMCTRL1 = 0x1a0; + if (nand_wait_stat(FMCSTAT_UNK20)) + return -1; + discard_dcache_range(dst, NAND_CHUNK_SIZE); + return 0; +} + +int nand_read_page(uint32_t bank, uint32_t page, void *databuf, + uint32_t *meta) +{ + struct nand_ecc_acc ecc = { false, 0, 0 }; + unsigned long spins; + uint32_t chunk; + + /* That sequence only knows 2KiB pages. Larger pages take the firmware's + * read sequence, which loops over the page's units. */ + if (nand_chip->pagesize != NAND_UNIT_SIZE) + { + struct nand_read r = { bank, page, databuf, meta, 0, 0 }; + + return nand_read_pages(&r, 1) ? -1 : r.ecc; + } + + nand_set_fmctrl0(bank); + if (nand_send_cmd(NAND_CMD_READ)) + return -1; + if (nand_send_addr_page(page)) + return -1; + if (nand_send_cmd(NAND_CMD_READ2)) + return -1; + if (nand_wait_ready()) + return -1; + if (nand_send_cmd(NAND_CMD_READ)) /* back to data output */ + return -1; + + for (chunk = 0; chunk < 4; chunk++) + { + FMCSTAT = FMCSTAT_UNK27; + if (nand_transfer_chunk(chunk, + (uint8_t *)databuf + chunk * NAND_CHUNK_SIZE, &ecc)) + return -1; + } + + /* Have the controller decode the spare metadata, as the firmware does + * after its chunk loop. The result appears in FMSYND5..7. */ + FMUNK78 = 0x5140; + FMUNK7C = 2; + spins = NAND_POLL_SPINS; + while (FMUNK7C & 2) + if (!--spins) + return -1; + if (meta) + { + meta[0] = FMSYND5; + meta[1] = FMSYND6; + meta[2] = FMSYND7; + } + + nand_clear_status(); + + /* The same classification the firmware's page read returns */ + if (!ecc.flagged) + return NAND_ECC_CLEAN; + if (ecc.unk810 & 1) + return NAND_ECC_FAILED; + if (ecc.transstat & 0x20) + return NAND_ECC_STATUS6; + return NAND_ECC_CORRECTED; +} + +/* Add one chunk's correction result to the bitmap used by Apple's firmware + * read program: bit 30 is uncorrectable, and low bit n-1 says an n-bit + * correction occurred. */ +static void nand_read_ecc_result(uint32_t *result) +{ + uint32_t ecc = FMUNK810; + + if (ecc & 1) + *result |= 1u << 30; + else + { + uint32_t count = (ecc >> 16) & 0xf; + if (count) + *result |= 1u << (count - 1); + } +} + +/* Transfer one page after its NAND page load has completed. This is the + * data and ECC path of the firmware's read program: four chunks per + * 2KiB unit, the last chunk of every unit but the last going out as the + * next unit's buffer is loaded, and the page's final chunk and erased-page + * check once at the end. */ +static int nand_read_loaded_page(struct nand_read *r, struct nand_read *next, + bool primed, uint32_t *result) +{ + static const uint32_t bufsel[3] = { 0x102, 0x201, 0x102 }; + static const uint32_t correct[4] = { 0x1101, 0x11201, 0x21101, 0x31201 }; + uint32_t units = nand_chip->pagesize / NAND_UNIT_SIZE; + uint32_t unit, chunk, stat; + unsigned long spins; + + if (!primed) + { + FMCTRL1 = 0x100e0; + if (nand_send_cmd(NAND_CMD_READ)) + return -1; + + /* Prime the first 16-byte and 512-byte controller stages. */ + FMDNUM = 0xf; + FMADDR2 = 0x10; + FMADDR6 = 0; + FMCTRL1 = 0x32; + if (nand_wait_stat(FMCSTAT_TRANSDONE)) + return -1; + FMDNUM = 0x1ff; + FMADDR2 = 1; + FMCTRL1 = 0xe2; + if (nand_wait_stat(FMCSTAT_TRANSDONE)) + return -1; + + FMCTRL0 |= FMCTRL0_AUTOXFER; + FMDATAW0 = (uint32_t)(intptr_t)r->buf; + } + + for (unit = 0; unit < units; unit++) + { + if (unit) + { + /* The previous unit's last chunk, and this unit's buffer */ + FMDNUM = 0xf; + FMADDR2 = 0x10; + FMADDR6 = 0; + FMCTRL1 = 0x32; + if (nand_wait_stat(FMCSTAT_TRANSDONE)) + return -1; + FMADDR2 = 0x201; + FMDNUM = 0x1ff; + FMCTRL1 = 0xe2; + FMTRANSSTAT = 0x1ff; + FMTRANS1 = 0x180; + FMTRANS0 = correct[3]; + if (nand_wait_reg(&FMTRANSSTAT, 1)) + return -1; + nand_read_ecc_result(result); + FMCTRL1 = 0x100; + if (nand_wait_stat(FMCSTAT_UNK20)) + return -1; + FMDATAW0 = (uint32_t)(intptr_t)r->buf + unit * NAND_UNIT_SIZE; + if (nand_wait_stat(FMCSTAT_TRANSDONE)) + return -1; + } + + for (chunk = 0; chunk < 3; chunk++) + { + FMDNUM = 0xf; + FMADDR2 = 0x10; + FMADDR6 = (chunk + 1) << 4; + FMCTRL1 = 0x32; + if (nand_wait_stat(FMCSTAT_TRANSDONE)) + return -1; + FMADDR2 = bufsel[chunk]; + FMDNUM = 0x1ff; + FMCTRL1 = 0xe2; + + FMTRANSSTAT = 0x1ff; + FMTRANS1 = 0x180; + FMTRANS0 = correct[chunk]; + if (nand_wait_reg(&FMTRANSSTAT, 1)) + return -1; + nand_read_ecc_result(result); + FMCTRL1 = 0x100; + if (nand_wait_stat(FMCSTAT_UNK20) + || nand_wait_stat(FMCSTAT_TRANSDONE)) + return -1; + } + } + + stat = FMCSTAT & FMCSTAT_UNK27; + FMCSTAT = FMCSTAT_UNK27; + if (!stat) + *result |= 1u << 29; + FMTRANSSTAT = 0x1ff; + FMTRANS1 = 0x180; + FMTRANS0 = correct[3]; + + if (next) + { + FMCTRL0 = (FMCTRL0 & ~FMCTRL0_CE_MASK) | FMCTRL0_CE(next->bank); + FMADDR2 = 1; + if (nand_send_cmd(NAND_CMD_READSTATUS)) + return -1; + FMCTRL1 = 0xca; + if (nand_wait_stat(FMCSTAT_STATUSREADY)) + return -1; + FMCTRL1 = 0x20; + } + if (nand_wait_reg(&FMTRANSSTAT, 1)) + return -1; + nand_read_ecc_result(result); + + if (!next) + { + FMDATAW1 = 7; + FMADDR2 = 0x200; + FMDNUM = 0x1ff; + FMCTRL1 = 0x1e0; + if (nand_wait_stat(FMCSTAT_UNK20)) + return -1; + } + + FMUNK78 = 0x3210; + FMUNK7C = 2; + spins = NAND_POLL_SPINS; + while (FMUNK7C & 2) + if (!--spins) + return -1; + if (r->meta) + { + r->meta[0] = FMSYND5; + r->meta[1] = FMSYND6; + r->meta[2] = FMSYND7; + } + + if (next) + { + FMCTRL1 = 0x100e0; + if (nand_send_cmd(NAND_CMD_READ)) + return -1; + FMDNUM = 0xf; + FMADDR2 = 0x10; + FMADDR6 = 0; + FMCTRL1 = 0x32; + if (nand_wait_stat(FMCSTAT_TRANSDONE)) + return -1; + FMADDR2 = 0x201; + FMDNUM = 0x1ff; + FMCTRL1 = 0xe2; + FMCTRL1 = 0x100; + if (nand_wait_stat(FMCSTAT_UNK20)) + return -1; + FMDATAW0 = (uint32_t)(intptr_t)next->buf; + if (nand_wait_stat(FMCSTAT_TRANSDONE)) + return -1; + } + return 0; +} + +int nand_read_pages(struct nand_read *r, unsigned int n) +{ + unsigned int first, end, i; + uint32_t used; + int status; + + if (!nand_ready || !n) + return -1; + for (i = 0; i < n; i++) + { + if (r[i].bank >= nand_banks + || r[i].page >= (uint32_t)nand_chip->blocks + * nand_chip->pagesperblock) + return -1; + commit_discard_dcache_range(r[i].buf, nand_chip->pagesize); + r[i].ecc = NAND_ECC_FAILED; + } + + FMCTRL1 = 0x0ff3f8e0; + FMCSTAT = 0x0ff00ffe; + FMCTRL0 = (NAND_TUNK1 << 16) | (NAND_TWP << 12) | FMCTRL0_UNK1 | 1; + + /* The FMSS program consumes the largest prefix containing no repeated + * bank, starts every page load in it, transfers those pages, then repeats + * with the remainder. */ + for (first = 0; first < n; first = end) + { + used = 0; + for (end = first; end < n && !(used & (1u << r[end].bank)); end++) + used |= 1u << r[end].bank; + + for (i = first; i < end; i++) + { + FMCTRL0 = (FMCTRL0 & ~FMCTRL0_CE_MASK) | FMCTRL0_CE(r[i].bank); + if (nand_send_cmd(NAND_CMD_READ) + || nand_send_addr_page(r[i].page) + || nand_send_cmd(NAND_CMD_READ2)) + goto fail; + } + + for (i = first; i < end; i++) + { + uint32_t result = 0; + + if (i == first) + FMCTRL0 = (FMCTRL0 & ~FMCTRL0_CE_MASK) + | FMCTRL0_CE(r[i].bank); + status = i == first ? nand_read_status() : NAND_STATUS_READY; + if (status < 0 + || nand_read_loaded_page(&r[i], i + 1 < end ? &r[i + 1] : NULL, + i != first, &result)) + goto fail; + r[i].result = result; + r[i].ecc = (result & (1u << 30)) ? NAND_ECC_FAILED + : (result & 0xff) ? NAND_ECC_CORRECTED + : NAND_ECC_CLEAN; + discard_dcache_range(r[i].buf, nand_chip->pagesize); + } + FMCTRL0 &= ~FMCTRL0_AUTOXFER; + FMCTRL0 &= ~FMCTRL0_CE_MASK; + } + return 0; + +fail: + FMCTRL0 &= ~FMCTRL0_AUTOXFER; + nand_clear_status(); + return -1; +} + +/* + * Programming and erasing. The original firmware does these with programs + * that the FMC's sequencer runs in hardware; this driver makes the register + * writes those programs do for a single page or block from the ARM. Tested + * on hardware: a page written, read back and erased again. + */ + +static int nand_finish(int status) +{ + nand_clear_status(); + if (status < 0) + return -1; + return (status & NAND_STATUS_FAIL) ? NAND_OP_FAILED : 0; +} + +int nand_erase_block(uint32_t bank, uint32_t block) +{ + if (!nand_ready || bank >= nand_banks || block >= nand_chip->blocks) + return -1; + + FMCTRL1 = 0x0ff3f8e0; + FMCSTAT = 0x0ff00ffe; + nand_set_fmctrl0(bank); + if (nand_send_cmd(NAND_CMD_ERASE)) + return nand_finish(-1); + FMANUM = 2; /* row address only */ + FMADDR0 = block * nand_chip->pagesperblock; + FMCTRL1 = FMCTRL1_DOTRANSADDR; + if (nand_wait_stat(FMCSTAT_ADDRDONE)) + return nand_finish(-1); + if (nand_send_cmd(NAND_CMD_ERASE2)) + return nand_finish(-1); + return nand_finish(nand_read_status()); +} + +/* Load a page's first chunk into the controller and encode its spare + * metadata. The rest of the page follows chunk by chunk as it goes out. */ +static int nand_load_spare(const uint32_t *meta) +{ + unsigned long spins = NAND_POLL_SPINS; + + FMSYND5 = meta[0]; + FMSYND6 = meta[1]; + FMSYND7 = meta[2]; + FMUNK78 = 0x3210; + FMUNK7C = 1; + while (FMUNK7C & 1) + if (!--spins) + return -1; + return nand_wait_stat(FMCSTAT_UNK20); +} + +/* Start the ECC engine on a chunk */ +static void nand_encode(uint32_t cmd) +{ + FMTRANSSTAT = 0x1ff; + FMTRANS1 = 0x180; + FMTRANS0 = cmd; +} + +/* READ STATUS on a bank of the current run: the chip enable is switched + * without touching the rest of FMCTRL0 */ +static int nand_run_status(uint32_t bank) +{ + FMCTRL0 = (FMCTRL0 & ~FMCTRL0_CE_MASK) | FMCTRL0_CE(bank); + return nand_read_status(); +} + +/* Program a run of pages, each with its 12 bytes of spare metadata, as the + * firmware's sequencer programs do: one plane at a time, or with two_plane + * two planes per command, as the chip's mode selects. The controller loads + * each + * 512-byte chunk from memory while the previous one goes out, a 4KiB page's + * second 2KiB unit included, and loads a page's first chunk while the page + * before it finishes, so the run never + * waits for the host. A chip is not waited for until it is given its next + * page: the others program in the meantime, and every chip used is checked + * at the end. With two_plane the pages come in pairs on one bank, plane 0 + * then plane 1. On failure *failbank names the bank that reported it. */ +int nand_write_pages(const struct nand_write *w, unsigned int n, + bool two_plane, uint32_t *failbank) +{ + /* Per chunk: the ECC engine's encode command, and which of the + * controller's buffers goes out while the next chunk loads */ + static const uint32_t encode[4] = { 0x01102, 0x11202, 0x21102, 0x31202 }; + static const uint32_t bufsel[4] = { 0x102, 0x201, 0x102, 0x201 }; + uint32_t chunks = nand_chip->pagesize / NAND_CHUNK_SIZE; + uint32_t used, bank, i, chunk; + int status; + + if (!nand_ready || !n || (two_plane && (n & 1))) + return -1; + for (i = 0; i < n; i++) + { + if (w[i].bank >= nand_banks + || w[i].page >= (uint32_t)nand_chip->blocks + * nand_chip->pagesperblock) + return -1; + /* The controller reads the data behind the cache's back */ + commit_dcache_range(w[i].buf, nand_chip->pagesize); + } + + FMCTRL1 = 0x0ff3f8e0; + FMCSTAT = 0x0ff00ffe; + FMCTRL0 = (NAND_TUNK1 << 16) | (NAND_TWP << 12) | FMCTRL0_UNK1 | 1; + FMCTRL0 = (FMCTRL0 & ~FMCTRL0_CE_MASK) | FMCTRL0_CE(w[0].bank); + used = 1 << w[0].bank; + + /* Chunk 0 of the first page into the controller */ + FMDATAW0 = (uint32_t)(intptr_t)w[0].buf; + FMCTRL0 |= FMCTRL0_AUTOXFER; + FMDNUM = 0x1ff; + FMADDR2 = 1; + FMCTRL1 = 0x2e0; + if (nand_load_spare(w[0].meta)) + goto fail; + nand_encode(encode[0]); + + for (i = 0; i < n; i++) + { + bool plane1 = two_plane && (i & 1); + bool last = i + 1 == n; + + if (nand_send_cmd(plane1 ? NAND_CMD_PROGRAM_PLANE1 : NAND_CMD_PROGRAM) + || nand_send_addr_page(w[i].page) + || nand_wait_reg(&FMTRANSSTAT, 1 << 0)) + goto fail; + + for (chunk = 0; chunk < chunks; chunk++) + { + bool pageend = chunk + 1 == chunks; + + if (pageend && last) + FMCTRL0 &= ~FMCTRL0_AUTOXFER; + + /* 16 bytes of ECC and spare */ + FMDNUM = 0xf; + FMADDR2 = 0x1000; + FMADDR7 = (chunk % 4) << 4; + FMCTRL1 = 0x34; + if (pageend && !last) + FMDATAW0 = (uint32_t)(intptr_t)w[i + 1].buf; + else if (chunk % 4 == 3 && !pageend) /* the next 2KiB unit */ + FMDATAW0 = (uint32_t)(intptr_t)w[i].buf + + (chunk + 1) * NAND_CHUNK_SIZE; + if (nand_wait_stat(FMCSTAT_TRANSDONE)) + goto fail; + + /* 512 bytes of data, and the next chunk in behind them */ + if (pageend && last) + { + FMDATAW1 = 7; + FMADDR2 = 0x200; + FMDNUM = 0x1ff; + FMCTRL1 = 0xe4; + if (nand_wait_stat(FMCSTAT_TRANSDONE)) + goto fail; + break; + } + FMADDR2 = bufsel[chunk % 4]; + FMDNUM = 0x1ff; + FMCTRL1 = 0x2e4; + if (pageend) + { + if (nand_load_spare(w[i + 1].meta)) + goto fail; + nand_encode(encode[0]); + if (nand_wait_stat(FMCSTAT_TRANSDONE)) + goto fail; + break; + } + if (nand_wait_stat(FMCSTAT_UNK20)) + goto fail; + nand_encode(encode[(chunk + 1) % 4]); + if (nand_wait_reg(&FMTRANSSTAT, 1 << 0) + || nand_wait_stat(FMCSTAT_TRANSDONE)) + goto fail; + } + + /* Plane 0 of a pair is queued; anything else starts programming */ + if (nand_send_cmd(two_plane && !plane1 ? NAND_CMD_PROGRAM_QUEUE + : NAND_CMD_PROGRAM2)) + goto fail; + if (last) + break; + + /* On to the next page's chip, which must have finished whatever it + * was given before */ + bank = w[i + 1].bank; + FMCTRL0 = (FMCTRL0 & ~FMCTRL0_CE_MASK) | FMCTRL0_CE(bank); + if (used & (1 << bank)) + { + status = nand_read_status(); + if (status < 0) + goto fail; + if (status & NAND_STATUS_FAIL) + goto failed; + } + used |= 1 << bank; + } + + for (bank = 0; bank < NAND_MAX_BANKS; bank++) + { + if (!(used & (1 << bank))) + continue; + status = nand_run_status(bank); + if (status < 0) + goto fail; + if (status & NAND_STATUS_FAIL) + goto failed; + } + FMCTRL0 &= ~FMCTRL0_CE_MASK; + nand_clear_status(); + return 0; + +failed: + if (failbank) + *failbank = __builtin_ctz(FMCTRL0 & 0x1fe) - 1; + FMCTRL0 &= ~FMCTRL0_AUTOXFER; + nand_clear_status(); + return NAND_OP_FAILED; + +fail: + if (failbank) + *failbank = __builtin_ctz(FMCTRL0 & 0x1fe) - 1; + FMCTRL0 &= ~FMCTRL0_AUTOXFER; + return nand_finish(-1); +} + +int nand_write_page(uint32_t bank, uint32_t page, const void *databuf, + const uint32_t *meta) +{ + struct nand_write w = { bank, page, databuf, meta }; + + return nand_write_pages(&w, 1, false, NULL); +} + +uint32_t nand_get_id(void) +{ + return nand_id; +} + +unsigned int nand_get_bank_count(void) +{ + return nand_banks; +} + +/* ---- Rockbox storage API ---- */ + +static int nand_init_chip(void) +{ + uint32_t bank, id; + unsigned int i; + int rc; + + nand_ready = false; + nand_banks = 0; + nand_chip = NULL; + + rc = nand_reset(0); + if (rc) + { + logf("nand: reset failed (%d)", rc); + return rc; + } + if (nand_identify(&id, NULL)) + { + logf("nand: identify failed"); + return NAND_ERR_IDENTIFY; + } + nand_id = id; + + /* Further banks count if they hold the same chip, contiguously. Like + * Apple's FIL, refuse a unit where another chip enable answers with a + * different part, or where a chip follows a missing one: mounting it + * with fewer banks would read the wrong blocks. */ + nand_banks = 1; + for (bank = 1; bank < NAND_MAX_BANKS; bank++) + { + if (nand_reset(bank) || nand_identify(&id, NULL) + || !id || id == 0xffffffff) + continue; + if (id != nand_id || bank != nand_banks) + { + logf("nand: bank %lu answers %08lx", (unsigned long)bank, + (unsigned long)id); + return NAND_ERR_MIXED; + } + nand_banks++; + } + + for (i = 0; i < ARRAYLEN(nand_chip_table); i++) + if (nand_chip_table[i].id == nand_id + && nand_chip_table[i].banks == nand_banks) + nand_chip = &nand_chip_table[i]; + if (!nand_chip) + { + /* A normal build carries only the chips Rockbox has been tested on, + * so a chip missing from the table is one nobody has validated */ + logf("nand: chip %08lx x %u is not supported", (unsigned long)nand_id, + nand_banks); + return NAND_ERR_UNSUPPORTED; + } + + nand_ready = true; + nand_touch(); + + /* The table can still hold a row that has not been through a write test + * (a NAND_TEST_READONLY build makes every row look that way): leave that + * chip alone too. The check image reads any chip it identifies. */ + if (!nand_get_geometry()->validated) + { + logf("nand: chip %08lx x %u is not validated", + (unsigned long)nand_id, nand_banks); + nand_ready = false; + return NAND_ERR_UNSUPPORTED; + } + + rc = ftl_init(); + if (rc) + { + logf("nand: FTL mount failed (%d)", rc); + nand_ready = false; + return NAND_ERR_FTL + rc; + } + return 0; +} int nand_init(void) { - // TODO - return 0; + int rc = nand_init_chip(); + + return rc; } void nand_spindown(int seconds) @@ -34,47 +1064,64 @@ void nand_spindown(int seconds) void nand_spin(void) { + nand_touch(); } #ifdef HAVE_STORAGE_FLUSH int nand_flush(void) { - return 0; + int rc; + + if (!nand_ready) + return -1; + rc = ftl_sync(); + if (rc) + panicf("Failed to unmount flash: %d", rc); + return rc; +} +#endif + +/* ---- the FTL's disk ---- */ +#ifdef HAVE_STORAGE_READONLY +bool nand_readonly(IF_MD_NONVOID(int drive)) +{ + IF_MD((void)drive); + return ftl_readonly_mount(); } #endif int nand_read_sectors(IF_MD(int drive,) sector_t start, int incount, void* inbuf) { - // TODO -#ifdef HAVE_MULTIDRIVE - (void) drive; -#endif - (void) start; - (void) incount; - (void) inbuf; - return 0; + IF_MD((void)drive); + int rc; + + if (!nand_ready) + return -1; + rc = ftl_read((uint32_t)start, incount, inbuf); + if (!rc) + nand_touch(); + return rc; } int nand_write_sectors(IF_MD(int drive,) sector_t start, int count, const void* outbuf) { - // TODO -#ifdef HAVE_MULTIDRIVE - (void) drive; -#endif - (void) start; - (void) count; - (void) outbuf; - return 0; + IF_MD((void)drive); + int rc; + + if (!nand_ready) + return -1; + rc = ftl_write((uint32_t)start, count, outbuf); + if (!rc) + nand_touch(); + return rc; } int nand_event(long id, intptr_t data) { - // TODO (void) id; (void) data; - return 0; } @@ -83,15 +1130,14 @@ void nand_get_info(IF_MD(int drive,) struct storage_info *info) { IF_MD((void)drive); info->sector_size = SECTOR_SIZE; - info->num_sectors = 0; - info->vendor = ""; - info->product = ""; - info->revision = ""; + info->num_sectors = ftl_num_sectors(); + info->vendor = "Apple"; + info->product = "iPod Nano 3G"; + info->revision = "1.0"; } #endif long nand_last_disk_activity(void) { - // TODO - return 0; + return nand_last_activity_value; } diff --git a/firmware/target/arm/s5l8702/ipodnano3g/nand-target.h b/firmware/target/arm/s5l8702/ipodnano3g/nand-target.h new file mode 100644 index 0000000000..ceda48d375 --- /dev/null +++ b/firmware/target/arm/s5l8702/ipodnano3g/nand-target.h @@ -0,0 +1,161 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2009 by Michael Sparmann + * + * Interface modelled on + * firmware/target/arm/s5l8700/ipodnano2g/nand-target.h. + * + * Copyright (C) 2026 by Andrew Rice + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef __NAND_TARGET_H__ +#define __NAND_TARGET_H__ + +#include +#include + +/* The smallest page, which is also the storage layer's sector: 4KiB-page + * chips hold two sectors per page */ +#define NAND_PAGE_SIZE 2048 +#define NAND_MAX_PAGE_SIZE 4096 + +/* The on-flash VFL suggests four banks on the 4GB model. FMCTRL0 has chip + * enable bits for eight. */ +#define NAND_MAX_BANKS 4 + +/* Non-negative results of nand_read_page(): the ECC verdict of Apple's + * the firmware's page read. Every written page reads as CORRECTED and every + * erased page + * as CLEAN (measured over a whole 4GB unit), so CORRECTED is the normal + * result for data. FAILED and STATUS6 are named from how the ROM derives + * them and have not been seen. */ +#define NAND_ECC_CLEAN 0 +#define NAND_ECC_CORRECTED 1 +#define NAND_ECC_FAILED 3 +#define NAND_ECC_STATUS6 6 + +/* Size of the spare metadata nand_read_page() returns, in 32-bit words. + * This is the 12-byte Whimory spare header (lpn/usn/type/eccmark). */ +#define NAND_META_WORDS 3 + +/* How a VFL block's planes sit on a bank (Apple's mode functions for the + * FIL, which the original firmware installs per chip mode) */ +#define NAND_LAYOUT_UNKNOWN 0 +#define NAND_LAYOUT_SINGLE 1 /* one block: mode 1 */ +#define NAND_LAYOUT_ADJACENT 2 /* block 2b + plane: modes 3, 8 */ +#define NAND_LAYOUT_HALVES 3 /* b + plane * blocks / 2: 2, 9, 12 */ +#define NAND_LAYOUT_BOTH 4 /* both of the above: mode 4 */ +#define NAND_LAYOUT_SPLIT13 5 /* Toshiba's 8320 blocks: mode 13 */ + +struct nand_geometry +{ + unsigned int banks; + unsigned int blocks; /* per bank */ + unsigned int pagesperblock; + unsigned int planes; /* physical blocks per bank in a VFL block */ + unsigned int userblocks; /* per bank, as Whimory counts them */ + unsigned int vflspares; /* VFL reserved blocks per plane */ + bool twoplane; /* whole rows may be programmed two-plane */ + unsigned int pagesize; /* bytes of data per page */ + unsigned int mode; /* Apple's chip table mode */ + unsigned int layout; /* NAND_LAYOUT_* */ + bool validated; /* may be mounted writable */ +}; + +/* Reset one bank (chip enable) and select it. Returns 0 or a NAND_ERR_*. */ +int nand_reset(uint32_t bank); + +/* Read the first four ID bytes of the bank the last nand_reset() selected. + * ext, if not NULL, gets the next four. Returns 0, or -1 on a timeout. */ +int nand_identify(uint32_t *id, uint32_t *ext); + +/* Read one page. meta may be NULL. Returns a negative number if the + * controller timed out, else one of NAND_ECC_*. */ +int nand_read_page(uint32_t bank, uint32_t page, void *databuf, + uint32_t *meta); + +/* One page of a read run. nand_read_pages() fills ecc with one of + * NAND_ECC_* when the controller completed the page. */ +struct nand_read +{ + uint32_t bank; + uint32_t page; + void *buf; + uint32_t *meta; + int ecc; + uint32_t result; /* the read program's result bitmap: bit 30 + * uncorrectable, bit 29 erased, bit n-1 an n-bit + * correction in some chunk */ +}; + +/* Read a run of pages with Apple's firmware read sequence, overlapping the + * NAND page-load time on the chip enables in the run. Returns 0 if every + * page completed or -1 on a controller timeout; inspect each page's ecc for + * its ECC verdict. */ +int nand_read_pages(struct nand_read *r, unsigned int n); + +/* Erase one block, or write one page with its NAND_META_WORDS words of spare + * metadata. Return 0, -1 if the controller timed out, or NAND_OP_FAILED if + * the chip reports a failure (the block should be treated as bad). */ +#define NAND_OP_FAILED (-2) +int nand_erase_block(uint32_t bank, uint32_t block); +int nand_write_page(uint32_t bank, uint32_t page, const void *databuf, + const uint32_t *meta); + +/* One page of a run: where it goes, its data and its spare metadata */ +struct nand_write +{ + uint32_t bank; + uint32_t page; + const void *buf; + const uint32_t *meta; +}; + +/* Program a run of pages at once, as Apple's firmware does: the chips + * program in parallel, each waited for only when it is given its next + * page, and with two_plane the pages come in pairs - plane 0 then plane 1 + * of one bank - programmed by one command each. Returns as + * nand_write_page(). After a failure *failbank (if not NULL) is the bank + * that was being programmed or reported it, and any page of the run may not + * have been written. */ +int nand_write_pages(const struct nand_write *w, unsigned int n, + bool two_plane, uint32_t *failbank); + +/* nand_init() results. -4 is retired: early builds used it for a ready + * bit timeout that turned out not to be an error. */ +enum nand_init_result +{ + NAND_ERR_RESET_CMD = -1, /* RESET never completed */ + NAND_ERR_IDENTIFY = -2, /* READ ID failed */ + NAND_ERR_READY_CMD = -3, /* READ STATUS never completed */ + NAND_ERR_UNSUPPORTED = -5, /* not a chip Rockbox has been tested on */ + NAND_ERR_MIXED = -6, /* chip enables answer with different ids */ + NAND_ERR_FTL = -100, /* plus ftl_init()'s code */ +}; + +/* The READ ID bytes bank 0 answered with, 0 before nand_init() */ +uint32_t nand_get_id(void); + +/* Number of banks (chip enables) nand_init() found holding the same chip + * as bank 0 */ +unsigned int nand_get_bank_count(void); + +/* Geometry of the attached chips, or NULL before nand_init() succeeded */ +const struct nand_geometry *nand_get_geometry(void); + +#endif /* __NAND_TARGET_H__ */ diff --git a/firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c index 08f0ef6ff9..f3a6aa521a 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c @@ -201,7 +201,8 @@ int pmu_firewire_present(void) } #endif -#if defined(BOOTLOADER) // XXX: from usb-s5l8702.c +/* XXX: from usb-s5l8702.c */ +#if defined(BOOTLOADER) && !defined(HAVE_BOOTLOADER_USB_MODE) #include "usb.h" static int usb_status = USB_EXTRACTED; From af9b65485bf8469f1a10525d3d8c014a5775187c Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sat, 19 Sep 2026 20:44:41 -0400 Subject: [PATCH 184/408] build: move '-mthumb-interwork' into the configure script When we request a thumb build on a non-thumb-only target, we hand compilation to a script that tries to build everything as thumb, falling back to non-thumb arm mode if the thumb build fails. In order for this to work, the compiler flag -mthumb-interwork is required. Move this definition out of the thumb-cc script, and into the configure script (ie along with the other target-specific definitions) Change-Id: Idc1f8caa088dbbf710b169b272de61cc0b7c9ca6 --- tools/configure | 1 + tools/thumb-cc.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/configure b/tools/configure index 7f3628b540..3e5331360b 100755 --- a/tools/configure +++ b/tools/configure @@ -4722,6 +4722,7 @@ fi if test "$ARG_ARM_THUMB" = "1"; then extradefines="$extradefines -DUSE_THUMB" + GCCOPTS="$GCCOPTS -mthumb-interwork" CC="$toolsdir/thumb-cc.py $CC" fi diff --git a/tools/thumb-cc.py b/tools/thumb-cc.py index 9e2b96c346..a7b31aead5 100755 --- a/tools/thumb-cc.py +++ b/tools/thumb-cc.py @@ -31,7 +31,11 @@ if '-o' in args and args.index('-o') < len(args) - 1: if len(args[args.index('-o') + 1].rsplit('.o', 1)) == 1: execv(args[0], args) # output doesn't end in .o -args.append('-mthumb-interwork') # thumb-interwork is required +# thumb-interwork is required; add if not present +if '-mthumb-interwork' not in args: + args += ['-mthumb-interwork']; + +# Try to compile in thumb mode first gcc = Popen(args + ['-mthumb'], stdout=PIPE, stderr=PIPE) (out, err) = gcc.communicate() From 305acca1f0226364d8352bdb9c60a6b1e6451c6c Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sat, 19 Sep 2026 21:13:51 -0400 Subject: [PATCH 185/408] dsp: Add missing .type , %function to arm asm Several symbols were missing these annotations so the compiler wasn't handling thumb interworking correctly. Should resolve tone control crashes seen on clipv1 and our handful of other 2MB armv5 targets that we build (mostly) in thumb mode. Change-Id: If8c533a5f10b6592a2d2bec519f2c4d6539d7a66 --- lib/rbcodec/dsp/dsp_arm.S | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/rbcodec/dsp/dsp_arm.S b/lib/rbcodec/dsp/dsp_arm.S index dea9eed000..e2eba46aab 100644 --- a/lib/rbcodec/dsp/dsp_arm.S +++ b/lib/rbcodec/dsp/dsp_arm.S @@ -191,6 +191,7 @@ channel_mode_proc_karaoke: */ .section .text, "ax",%progbits .global crossfeed_process + .type crossfeed_process, %function crossfeed_process: @ input: r0 = this, r1 = buf_p @ unfortunately, we ended up in a bit of a register squeeze here, and need @@ -256,6 +257,7 @@ crossfeed_process: */ .section .text .global crossfeed_meier_process + .type crossfeed_meier_process, %function crossfeed_meier_process: @ input: r0 = this, r1 = buf_p ldr r1, [r1] @ r1 = buf = *buf_p; @@ -296,6 +298,7 @@ crossfeed_meier_process: */ .section .text, "ax",%progbits .global resample_hermite + .type resample_hermite, %function resample_hermite: @input: r0 = data, r1 = src, r2 = dst stmfd sp!, { r0-r2, r4-r11, lr } @ stack parms, modified regs @@ -575,6 +578,7 @@ pga_process: .section .text, "ax",%progbits #endif .global filter_process + .type filter_process, %function filter_process: @input: r0 = f, r1 = buf, r2 = count, r3 = channels stmfd sp!, { r4-r11, lr } @ save all clobbered regs From f834e02af87aacb43e23cd15803ce3d10170a3ab Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sat, 19 Sep 2026 21:23:33 -0400 Subject: [PATCH 186/408] pacbox: Add missing function annotation to arm asm functions Change-Id: If920186cfdc4e22ec9a6dc154b0d2979b9a1f541 --- apps/plugins/pacbox/pacbox_arm.S | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/plugins/pacbox/pacbox_arm.S b/apps/plugins/pacbox/pacbox_arm.S index 5b06af53c1..70d4a19724 100644 --- a/apps/plugins/pacbox/pacbox_arm.S +++ b/apps/plugins/pacbox/pacbox_arm.S @@ -25,6 +25,7 @@ .section .icode,"ax",%progbits .global blit_display + .type blit_display, %function /* void blit_display(fb_data* lcd_framebuffer, unsigned char* vbuf) From e4c010be985a12a04a80cbc209b1aad971eb2204 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Sat, 19 Sep 2026 07:52:02 +1000 Subject: [PATCH 187/408] ipodnano3g: NAND check image for validating other chips Rockbox only drives NAND parts proven on hardware, and this tree has one model to prove them on. This is the image that lets an owner of another Nano 3G supply what validating theirs takes: a bootloader built with -DNAND_CHECK and run from DFU, which never touches the NOR flash or writes to the NAND. Testing evidence: firmware/target/arm/s5l8702/ipodnano3g/TESTING.md. Co-authored-by: Claude Opus 5 Change-Id: I46328b69f8011337790f35a195de97b5bcf347a5 --- bootloader/ipod-s5l87xx.c | 119 +++++++ firmware/SOURCES | 3 + .../target/arm/s5l8702/ipodnano3g/TESTING.md | 7 + .../s5l8702/ipodnano3g/nand-check-nano3g.c | 316 ++++++++++++++++++ .../arm/s5l8702/ipodnano3g/nand-nano3g.c | 70 +++- .../arm/s5l8702/ipodnano3g/nand-target.h | 22 ++ utils/ipodnano3g/nandcheck/build.sh | 55 +++ 7 files changed, 590 insertions(+), 2 deletions(-) create mode 100644 firmware/target/arm/s5l8702/ipodnano3g/nand-check-nano3g.c create mode 100755 utils/ipodnano3g/nandcheck/build.sh diff --git a/bootloader/ipod-s5l87xx.c b/bootloader/ipod-s5l87xx.c index 16dbb1cce2..c484ae97d8 100644 --- a/bootloader/ipod-s5l87xx.c +++ b/bootloader/ipod-s5l87xx.c @@ -48,6 +48,9 @@ #ifdef IPOD_NANO3G #include "nand-target.h" #endif +#ifdef NAND_CHECK +#include "string-extra.h" +#endif #ifdef HAVE_SERIAL #include "serial.h" #endif @@ -147,6 +150,117 @@ static void usb_mode(void) } #endif /* HAVE_BOOTLOADER_USB_MODE */ +#if defined(NAND_CHECK) && defined(HAVE_BOOTLOADER_USB_MODE) \ + && !defined(S5L87XX_DEVELOPMENT_BOOTLOADER) +/* The contributor NAND check (build with -DNAND_CHECK, run from DFU): show + * what the driver identified and how the read-only mount went, add the + * unit's model and firmware version, then serve the raw NAND over USB for + * utils/ipodnano3g/nandcheck/nandcheck.py. Nothing is written to the NAND. */ +/* The bootloader's backlight calls are stubs, and the check's screen has + * come up too dim to read at the default level. Full brightness draws more + * than a battery-less unit's USB power alone can supply and can keep it + * from booting at all, so this stops at the default rather than the max. */ +static void nand_check_light(void) +{ + backlight_hw_brightness(DEFAULT_BRIGHTNESS_SETTING); + backlight_hw_on(); +} + +/* The report's key lines, short enough for the screen; the whole report + * goes to nandcheck.py */ +static void nand_check_print(const char *report) +{ + static const char *const keys[] = { + "banks", "row", "mode", "pagesize", "validated", "ftl", "verdict", + "model", "swvr", + }; + static char text[SECTOR_SIZE]; + char *p, *nl, *sp; + size_t i; + + strlcpy(text, report, sizeof(text)); + for (p = text; (nl = strchr(p, '\n')); p = nl + 1) + { + *nl = '\0'; + sp = strchr(p, ' '); + if (!sp) + continue; + if (!strncmp(p, "ids ", 4)) + { + /* bank 0's id; the others are in the report */ + printf("id %.8s", sp + 1); + continue; + } + for (i = 0; i < ARRAYLEN(keys); i++) + if ((size_t)(sp - p) == strlen(keys[i]) + && !strncmp(p, keys[i], sp - p)) + printf("%s", p); + } +} + +static void nand_check(void) +{ + static struct SysCfg syscfg; + char line[64]; + int rc; + ssize_t n; + + nand_check_light(); + snprintf(line, sizeof(line), "battery %dmV", _battery_voltage()); + rc = storage_init(); + /* USB mode unmounts every volume when the host configures the device, + * through the file system's locks and object lists */ + filesystem_init(); + + lcd_set_foreground(LCD_RBYELLOW); + printf("Nano 3G NAND check"); + lcd_set_foreground(LCD_WHITE); + printf("%s", line); + nand_check_note(line); + nand_check_light(); + if (rc) + printf("storage_init %d", rc); + + /* Which unit this is, but not its serial number */ + n = syscfg_read(&syscfg); + if (n != -1) + { + size_t i, count = MIN(syscfg.header.num_entries, SYSCFG_MAX_ENTRIES); + + for (i = 0; i < count; i++) + { + const struct SysCfgEntry *e = &syscfg.entries[i]; + const uint32_t *w = (const uint32_t *)e->data; + + if (e->tag == SYSCFG_TAG_MODN) + snprintf(line, sizeof(line), "model %.16s", e->data); + else if (e->tag == SYSCFG_TAG_SWVR) + snprintf(line, sizeof(line), "swvr %.16s", e->data); + else if (e->tag == SYSCFG_TAG_HWVR) + snprintf(line, sizeof(line), "hwvr %08lx", + (unsigned long)w[1]); + else + continue; + nand_check_note(line); + } + } + + nand_check_print(nand_check_report()); + lcd_set_foreground(LCD_RBYELLOW); + printf("Photo, then connect USB"); + printf("and run nandcheck.py"); + lcd_set_foreground(LCD_WHITE); + /* One USB session: usb_mode() starts power management each time, so it + * must not run twice */ + usb_mode(); + nand_check_light(); + printf("Done. Hold MENU+SELECT"); + printf("to restart"); + while (1) + sleep(HZ); +} +#endif + void fatal_error(int err) { verbose = true; @@ -873,6 +987,11 @@ void main(void) devel_menu(); #endif /* S5L87XX_DEVELOPMENT_BOOTLOADER */ +#if defined(NAND_CHECK) && defined(HAVE_BOOTLOADER_USB_MODE) \ + && !defined(S5L87XX_DEVELOPMENT_BOOTLOADER) + nand_check(); +#endif + #ifndef S5L87XX_DEVELOPMENT_BOOTLOADER if (rc == 0) { #if (CONFIG_STORAGE & STORAGE_ATA) diff --git a/firmware/SOURCES b/firmware/SOURCES index 5db95ee45a..0c36888baa 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1636,6 +1636,9 @@ target/arm/s5l8702/ipodnano3g/powermgmt-nano3g.c target/arm/s5l8702/ipodnano3g/power-nano3g.c target/arm/s5l8702/ipodnano3g/nand-nano3g.c target/arm/s5l8702/ipodnano3g/ftl-nano3g.c +#ifdef NAND_CHECK +target/arm/s5l8702/ipodnano3g/nand-check-nano3g.c +#endif #if (CONFIG_RTC == RTC_NANO3G) target/arm/s5l8702/ipodnano3g/rtc-nano3g.c // TODO: -> drivers/rtc/rtc_xxx.c ??? #endif diff --git a/firmware/target/arm/s5l8702/ipodnano3g/TESTING.md b/firmware/target/arm/s5l8702/ipodnano3g/TESTING.md index 318110ed84..88702883a4 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/TESTING.md +++ b/firmware/target/arm/s5l8702/ipodnano3g/TESTING.md @@ -14,3 +14,10 @@ audio starvation. The 8GB Hynix A555D5AD row's evidence is Robin Tschirschnitz's (delacor): measured and run on their own unit, not this one. + +## NAND check image + +Tested on the 4GB unit from DFU: host sees "Nano 3G NAND", 2113537 x +2048-byte sectors, write protect on. `nandcheck.py collect` reads it in +seven minutes on Linux and Windows, no ECC failures or timeouts, and the +archive passes the host FTL suite. diff --git a/firmware/target/arm/s5l8702/ipodnano3g/nand-check-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/nand-check-nano3g.c new file mode 100644 index 0000000000..9509e1c0b8 --- /dev/null +++ b/firmware/target/arm/s5l8702/ipodnano3g/nand-check-nano3g.c @@ -0,0 +1,316 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Andrew Rice + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* + * The contributor check image (a bootloader built with -DNAND_CHECK, run + * from DFU; utils/ipodnano3g/nandcheck/build.sh builds it). It identifies + * the chip, tries the read-only mount, and serves a raw, read-only view of + * the NAND over USB for utils/ipodnano3g/nandcheck/nandcheck.py, in + * SECTOR_SIZE sectors: + * + * 0 a text report, NUL padded + * 1 + block * banks + bank that block's spare records: per page the + * 12 spare bytes and a result word, little + * endian: -1 if the read timed out, else + * NAND_ECC_* | the most bits the + * controller corrected in a chunk << 8 | + * 0x10000 if it found the page erased + * metaend + page * secperpage page data, bank by bank + * + * Nothing here writes to the NAND. This file is built only in a NAND_CHECK + * build, where it provides the storage API the driver (nand-nano3g.c) then + * leaves out: the disk served is the raw NAND, not the FTL's. + */ + +#include "config.h" +#include "system.h" +#include "mv.h" +#include "storage.h" +#include "nand-target.h" +#include "ftl-target.h" +#include +#include +#include +#include +#include "version.h" + +#define NAND_CHECK_RECORD 16 +#define NAND_CHECK_BATCH 8 /* pages read per bank per driver call */ +/* A block's records fill one sector: every row has 128 pages per block */ +#define NAND_CHECK_PPB (SECTOR_SIZE / NAND_CHECK_RECORD) + +static uint32_t nand_check_ids[NAND_MAX_BANKS]; +static uint32_t nand_check_ext[NAND_MAX_BANKS]; /* ID bytes 5-8 */ +static int nand_check_len; /* report bytes used */ +static char nand_check_text[SECTOR_SIZE]; +static uint8_t nand_check_spares[NAND_MAX_BANKS][SECTOR_SIZE]; +static uint32_t nand_check_block = 0xffffffff; +static uint32_t nand_check_page = 0xffffffff; +static uint8_t nand_check_buf[NAND_MAX_BANKS * NAND_CHECK_BATCH + * NAND_MAX_PAGE_SIZE] STORAGE_ALIGN_ATTR; +/* The chip to serve, taken from the driver at init: without one only the + * report is served */ +static struct nand_geometry nand_check_geo; +static bool nand_check_have_chip; + +/* rc is nand_init_chip()'s result: 0, a NAND_ERR_*, or NAND_ERR_FTL plus + * ftl_init()'s result */ +void nand_check_init(int rc) +{ + const struct nand_geometry *g; + uint32_t bank; + int ftl_rc = rc <= NAND_ERR_FTL ? rc - NAND_ERR_FTL : rc ? 1 : 0; + int n; + + for (bank = 0; bank < NAND_MAX_BANKS; bank++) + if (nand_reset(bank) || nand_identify(&nand_check_ids[bank], + &nand_check_ext[bank])) + nand_check_ids[bank] = nand_check_ext[bank] = 0; + /* Serve the raw NAND of any chip the driver identified, mounted or not. + * The raw view keeps a block's records in one sector, so a chip whose + * blocks are another size is reported but not read. */ + g = nand_check_use_chip(NAND_CHECK_PPB); + nand_check_have_chip = g != NULL; + if (g) + nand_check_geo = *g; + + n = snprintf(nand_check_text, sizeof(nand_check_text), + "nano3g-nandcheck 1\n" + "version %s\n" + "ids %08lx %08lx %08lx %08lx\n" + "ext %08lx %08lx %08lx %08lx\n" + "banks %u\n" + "nand %d\n", + rbversion, (unsigned long)nand_check_ids[0], + (unsigned long)nand_check_ids[1], + (unsigned long)nand_check_ids[2], + (unsigned long)nand_check_ids[3], + (unsigned long)nand_check_ext[0], + (unsigned long)nand_check_ext[1], + (unsigned long)nand_check_ext[2], + (unsigned long)nand_check_ext[3], nand_get_bank_count(), + rc > NAND_ERR_FTL ? rc : 0); + if (!g) + n += snprintf(nand_check_text + n, sizeof(nand_check_text) - n, + "row none\n"); + else + n += snprintf(nand_check_text + n, sizeof(nand_check_text) - n, + "row %d\n" + "mode %u\n" + "pagesize %u\n" + "blocks %u\n" + "ppb %u\n" + "userblocks %u\n" + "planes %u\n" + "vflspares %u\n" + "validated %d\n" + "ftl %d\n" + "sectors %lu\n", + nand_get_chip_row(), g->mode, + g->pagesize, g->blocks, g->pagesperblock, + g->userblocks, g->planes, g->vflspares, + g->validated, ftl_rc, + (unsigned long)ftl_num_sectors()); + n += snprintf(nand_check_text + n, sizeof(nand_check_text) - n, + "verdict %s\n", + rc == NAND_ERR_UNSUPPORTED ? "chip not in the chip table" + : rc == NAND_ERR_MIXED ? "different chips on the chip enables" + : !g ? "NAND did not answer" + : ftl_rc == 0 ? (g->validated ? "OK, validated chip" + : "OK, needs validation") + : ftl_rc == -2 ? "geometry beyond FTL" + : ftl_rc == -3 ? "no VFL context" + : ftl_rc == -4 ? "layout not supported" + : ftl_rc == -5 ? "VFL does not fit row" + : "mount failed"); + nand_check_len = MIN(n, (int)sizeof(nand_check_text) - 1); +} + +const char *nand_check_report(void) +{ + return nand_check_text; +} + +void nand_check_note(const char *line) +{ + nand_check_len += snprintf(nand_check_text + nand_check_len, + sizeof(nand_check_text) - nand_check_len, + "%s\n", line); + nand_check_len = MIN(nand_check_len, (int)sizeof(nand_check_text) - 1); +} + +/* The first sector of page data: the report and every block's records */ +static uint32_t nand_check_metaend(void) +{ + return 1 + nand_check_geo.blocks * nand_check_geo.banks; +} + +static uint64_t nand_check_sector_count(void) +{ + if (!nand_check_have_chip) + return 1; + return nand_check_metaend() + + (uint64_t)nand_check_geo.banks * nand_check_geo.blocks + * nand_check_geo.pagesperblock + * (nand_check_geo.pagesize / SECTOR_SIZE); +} + +/* Every bank's spare records for one block, reading the banks in parallel */ +static void nand_check_load_spares(uint32_t block) +{ + static struct nand_read r[NAND_MAX_BANKS * NAND_CHECK_BATCH]; + static uint32_t meta[NAND_MAX_BANKS * NAND_CHECK_BATCH][NAND_META_WORDS]; + uint32_t ppb = nand_check_geo.pagesperblock, p, q, bank, i, n; + + for (p = 0; p < ppb; p += NAND_CHECK_BATCH) + { + n = 0; + for (q = p; q < p + NAND_CHECK_BATCH && q < ppb; q++) + for (bank = 0; bank < nand_check_geo.banks; bank++, n++) + { + r[n].bank = bank; + r[n].page = block * ppb + q; + r[n].buf = nand_check_buf + n * nand_check_geo.pagesize; + r[n].meta = meta[n]; + } + if (nand_read_pages(r, n)) + for (i = 0; i < n; i++) + r[i].ecc = -1; + for (i = 0; i < n; i++) + { + uint8_t *rec = nand_check_spares[r[i].bank] + + (r[i].page % ppb) * NAND_CHECK_RECORD; + int32_t ecc = r[i].ecc; + uint32_t bits; + + if (ecc >= 0) + { + for (bits = 8; bits && !(r[i].result & (1u << (bits - 1))); + bits--); + ecc |= bits << 8; + if (r[i].result & (1u << 29)) + ecc |= 0x10000; + } + + memcpy(rec, meta[i], NAND_META_WORDS * 4); + memcpy(rec + 12, &ecc, 4); + } + } + nand_check_block = block; + nand_check_page = 0xffffffff; +} + +static void nand_check_sector(uint32_t sector, uint8_t *dst) +{ + uint32_t metaend, spp, perbank, g; + + if (sector == 0) + { + memcpy(dst, nand_check_text, SECTOR_SIZE); + return; + } + if (!nand_check_have_chip) + { + memset(dst, 0, SECTOR_SIZE); + return; + } + metaend = nand_check_metaend(); + spp = nand_check_geo.pagesize / SECTOR_SIZE; + perbank = nand_check_geo.blocks * nand_check_geo.pagesperblock; + if (sector < metaend) + { + uint32_t block = (sector - 1) / nand_check_geo.banks; + + if (block != nand_check_block) + nand_check_load_spares(block); + memcpy(dst, nand_check_spares[(sector - 1) % nand_check_geo.banks], + SECTOR_SIZE); + } + else + { + g = (sector - metaend) / spp; + if (g != nand_check_page) + { + /* Data as the chip gives it, whatever the ECC verdict: the spare + * records say which pages failed */ + nand_read_page(g / perbank, g % perbank, nand_check_buf, NULL); + nand_check_page = g; + nand_check_block = 0xffffffff; + } + memcpy(dst, nand_check_buf + (sector - metaend) % spp * SECTOR_SIZE, + SECTOR_SIZE); + } +} + +/* ---- Rockbox storage API: the check's read-only raw NAND ---- */ + +int nand_read_sectors(IF_MD(int drive,) sector_t start, int incount, + void* inbuf) +{ + IF_MD((void)drive); + uint8_t *buf = inbuf; + + if (start + incount > nand_check_sector_count()) + return -1; + while (incount--) + { + nand_check_sector((uint32_t)start++, buf); + buf += SECTOR_SIZE; + } + nand_spin(); + return 0; +} + +int nand_write_sectors(IF_MD(int drive,) sector_t start, int count, + const void* outbuf) +{ + IF_MD((void)drive); + (void)start; + (void)count; + (void)outbuf; + return -1; +} + +#ifdef HAVE_STORAGE_READONLY +bool nand_readonly(IF_MD_NONVOID(int drive)) +{ + IF_MD((void)drive); + return true; +} +#endif + +int nand_event(long id, intptr_t data) +{ + (void)id; + (void)data; + return 0; +} + +#ifdef STORAGE_GET_INFO +void nand_get_info(IF_MD(int drive,) struct storage_info *info) +{ + IF_MD((void)drive); + info->sector_size = SECTOR_SIZE; + info->num_sectors = nand_check_sector_count(); + info->vendor = "Rockbox"; + info->product = "Nano 3G NAND"; + info->revision = "chk1"; +} +#endif diff --git a/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c index b4d48fad6a..4b5c5b699f 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c @@ -34,6 +34,10 @@ * - Only chips validated on hardware are driven; nand_init() refuses any * other chip (see nand_chip_table). * - Storage goes through Apple's FTL (ftl-nano3g.c). + * + * A build with -DNAND_CHECK is the contributor check image: it serves the + * raw NAND read-only instead of the FTL's disk, so the storage API below is + * left to nand-check-nano3g.c and this file keeps only the chip. */ #include "config.h" @@ -100,12 +104,42 @@ struct nand_chip_info * * The mode decides how the VFL groups physical blocks (nand_mode_planes() * and nand_mode_layout()) and which program writes two planes at once. Only - * validated rows are driven; nand_init() refuses any other chip. */ + * validated rows are driven; nand_init() refuses any other chip, except in + * the check image, which reads a chip without mounting it writable. */ static const struct nand_chip_info nand_chip_table[] = { /* Hynix: the 4GB unit this port was developed on, and the 8GB one */ { 0xA514D3AD, 4, 8, 4096, 128, 2048, 3872, true }, { 0xA555D5AD, 4, 8, 8192, 128, 2048, 7744, true }, +#ifdef NAND_CHECK + /* The rest of the chips the original firmware knows. They are here for + * the check image alone, which identifies a chip and reads it to + * collect what validating it needs; a normal build does not carry them, + * so Rockbox cannot drive a chip nobody has tested. A row moves above + * this line, with its validated flag set, once someone has run the + * check and a write test on that chip. */ + /* Micronas (ITT Intermetall, acquired by TDK 2016): 0xEC, not Samsung */ + { 0xB614D5EC, 2, 8, 4096, 128, 4096, 3872, false }, /* reported */ + { 0xB614D5EC, 4, 1, 4096, 128, 4096, 3872, false }, + { 0x2555D5EC, 4, 9, 8192, 128, 2048, 7744, false }, + /* Hynix */ + { 0xB614D5AD, 4, 1, 4096, 128, 4096, 3872, false }, + /* Toshiba */ + { 0xA585D598, 2, 13, 8320, 128, 2048, 7744, false }, + { 0xA585D598, 4, 13, 8320, 128, 2048, 7744, false }, + { 0xBA94D598, 2, 12, 4096, 128, 4096, 3872, false }, + { 0xBA94D598, 4, 1, 4096, 128, 4096, 3872, false }, + /* Intel */ + { 0xA5D5D589, 2, 4, 8192, 128, 2048, 7744, false }, /* reported */ + { 0xA5D5D589, 4, 2, 8192, 128, 2048, 7744, false }, /* reported */ + { 0x3E94D589, 2, 3, 4096, 128, 4096, 3872, false }, /* reported */ + { 0x3ED5D789, 2, 2, 8192, 128, 4096, 7744, false }, + /* Micron */ + { 0xA5D5D52C, 2, 4, 8192, 128, 2048, 7744, false }, + { 0xA5D5D52C, 4, 2, 8192, 128, 2048, 7744, false }, + { 0x3E94D52C, 2, 3, 4096, 128, 4096, 3872, false }, + { 0x3ED5D72C, 2, 2, 8192, 128, 4096, 7744, false }, +#endif }; /* Physical blocks per bank in one VFL block: the original firmware's mode * switch uses 1 for mode 1, 4 for mode 4, and 2 for every other mode */ @@ -969,6 +1003,26 @@ unsigned int nand_get_bank_count(void) return nand_banks; } +#ifdef NAND_CHECK +/* Hooks for the contributor check image (nand-check-nano3g.c), which reads + * the raw NAND itself and serves the storage API in its place */ + +int nand_get_chip_row(void) +{ + return nand_chip ? (int)(nand_chip - nand_chip_table) : -1; +} + +const struct nand_geometry *nand_check_use_chip(unsigned int pagesperblock) +{ + /* Keep whatever was identified readable, mounted or not, but drop a + * chip whose blocks do not hold the pages the check's layout expects */ + if (nand_chip && nand_chip->pagesperblock != pagesperblock) + nand_chip = NULL; + nand_ready = nand_chip != NULL; + return nand_get_geometry(); +} +#endif /* NAND_CHECK */ + /* ---- Rockbox storage API ---- */ static int nand_init_chip(void) @@ -1029,6 +1083,7 @@ static int nand_init_chip(void) nand_ready = true; nand_touch(); +#ifndef NAND_CHECK /* The table can still hold a row that has not been through a write test * (a NAND_TEST_READONLY build makes every row look that way): leave that * chip alone too. The check image reads any chip it identifies. */ @@ -1039,6 +1094,7 @@ static int nand_init_chip(void) nand_ready = false; return NAND_ERR_UNSUPPORTED; } +#endif rc = ftl_init(); if (rc) @@ -1054,6 +1110,12 @@ int nand_init(void) { int rc = nand_init_chip(); +#ifdef NAND_CHECK + /* The check image reports whatever happened and serves the raw NAND of + * any chip it identified, mounted or not, so init always succeeds */ + nand_check_init(rc); + rc = 0; +#endif return rc; } @@ -1081,7 +1143,10 @@ int nand_flush(void) } #endif -/* ---- the FTL's disk ---- */ +/* ---- the FTL's disk. A NAND_CHECK build serves the raw NAND instead, from + * nand-check-nano3g.c, which defines these five entry points in place of + * the ones below ---- */ +#ifndef NAND_CHECK #ifdef HAVE_STORAGE_READONLY bool nand_readonly(IF_MD_NONVOID(int drive)) { @@ -1136,6 +1201,7 @@ void nand_get_info(IF_MD(int drive,) struct storage_info *info) info->revision = "1.0"; } #endif +#endif /* !NAND_CHECK */ long nand_last_disk_activity(void) { diff --git a/firmware/target/arm/s5l8702/ipodnano3g/nand-target.h b/firmware/target/arm/s5l8702/ipodnano3g/nand-target.h index ceda48d375..af9420e22b 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/nand-target.h +++ b/firmware/target/arm/s5l8702/ipodnano3g/nand-target.h @@ -158,4 +158,26 @@ unsigned int nand_get_bank_count(void); /* Geometry of the attached chips, or NULL before nand_init() succeeded */ const struct nand_geometry *nand_get_geometry(void); +#ifdef NAND_CHECK +/* Between the driver and the contributor check image + * (nand-check-nano3g.c), which in this build serves the raw NAND as the + * storage device in the FTL disk's place */ + +/* The driver's: which row of Apple's chip table nand_init() matched, or -1 */ +int nand_get_chip_row(void); + +/* The driver's: hand the check the identified chip, so its raw pages stay + * readable whether or not the mount succeeded. A chip whose blocks do not + * hold pagesperblock pages does not suit the check's sector layout and is + * dropped. Returns the geometry to serve, or NULL if there is none. */ +const struct nand_geometry *nand_check_use_chip(unsigned int pagesperblock); + +/* The check's: build the report from nand_init()'s result */ +void nand_check_init(int rc); +/* The check's report: what was identified and how the mount went */ +const char *nand_check_report(void); +/* Add a line to the report */ +void nand_check_note(const char *line); +#endif + #endif /* __NAND_TARGET_H__ */ diff --git a/utils/ipodnano3g/nandcheck/build.sh b/utils/ipodnano3g/nandcheck/build.sh new file mode 100755 index 0000000000..e8151d45cc --- /dev/null +++ b/utils/ipodnano3g/nandcheck/build.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# Build the iPod Nano 3G NAND check image from this Rockbox tree. +# +# build.sh check [OUTDIR] +# +# The check identifies the chip, tries a read-only mount, then serves the +# raw NAND over USB for nandcheck.py. +# +# Needs an ARM toolchain (Rockbox's from tools/rockboxdev.sh, or +# arm-none-eabi-gcc) and, for mks5lboot, libusb-1.0 development files. +# Writes nano3g-WHAT.dfu, to send with +# utils/mks5lboot/mks5lboot --dfusend nano3g-WHAT.dfu +# from Apple's DFU mode, and nano3g-WHAT-wind3x.dfu if wInd3x is on PATH. +# Nothing is installed on the iPod and nothing is written to its NAND. +set -e + +what=${1:-check} +case "$what" in + check) define=NAND_CHECK ;; + *) echo "usage: $0 check [OUTDIR]" >&2; exit 2 ;; +esac + +root=$(cd "$(dirname "$0")/../../.." && pwd) +mkdir -p "${2:-.}" +out=$(cd "${2:-.}" && pwd) +build="$root/build-nano3g-$what" +jobs=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2) + +# Rockbox's own toolchain if it is on PATH, else a distribution's +# arm-none-eabi one; CROSS=prefix- overrides both +if [ -z "$CROSS" ] && ! command -v arm-elf-eabi-gcc > /dev/null 2>&1 \ + && command -v arm-none-eabi-gcc > /dev/null 2>&1; then + CROSS=arm-none-eabi- +fi + +mkdir -p "$build" +cd "$build" +if [ ! -f Makefile ]; then + "$root/tools/configure" --target=ipodnano3g --type=b \ + ${CROSS:+--compiler-prefix=$CROSS} < /dev/null > configure.log 2>&1 +fi +# The image is the Nano 3G bootloader built with one extra define +if ! grep -q -- "-D$define" Makefile; then + sed "s|^export EXTRA_DEFINES=\(.*\)|export EXTRA_DEFINES=\1 -D$define|" \ + Makefile > Makefile.new && mv Makefile.new Makefile +fi +make -j"$jobs" + +make -C "$root/utils/mks5lboot" > /dev/null +"$root/utils/mks5lboot/mks5lboot" --mkdfu-raw bootloader.bin \ + "$out/nano3g-$what.dfu" +if command -v wInd3x > /dev/null 2>&1; then + wInd3x makedfu -k n3g bootloader.bin "$out/nano3g-$what-wind3x.dfu" +fi +echo "built $out/nano3g-$what.dfu" From 76f8925d237b88e53854e9782e4ad87294bc8da2 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Sat, 19 Sep 2026 07:52:18 +1000 Subject: [PATCH 188/408] ipodnano3g: power, RTC, backlight, battery and audio Fills in the stubs the Nano 3G port was left with, taking each from how the original firmware drives the same hardware. Testing evidence: firmware/target/arm/s5l8702/ipodnano3g/TESTING.md. Co-authored-by: Claude Opus 5 Change-Id: Iea5314769502f5941a176600869591756c5e3233 --- apps/settings_list.c | 2 +- firmware/SOURCES | 4 +- firmware/drivers/audio/wm8975.c | 47 ++++++++++++- firmware/export/audiohw.h | 2 +- firmware/export/config/ipodnano3g.h | 15 ++--- firmware/export/wm8975.h | 3 + firmware/export/wmcodec.h | 6 ++ firmware/target/arm/ipod/button-clickwheel.c | 5 ++ .../target/arm/s5l8702/ipodnano3g/TESTING.md | 10 +++ .../arm/s5l8702/ipodnano3g/adc-nano3g.c | 43 ++++++++++-- .../arm/s5l8702/ipodnano3g/backlight-nano3g.c | 7 +- .../arm/s5l8702/ipodnano3g/cscodec-nano3g.c | 67 ------------------- .../arm/s5l8702/ipodnano3g/pmu-nano3g.c | 51 ++++++++++---- .../arm/s5l8702/ipodnano3g/pmu-target.h | 18 ++++- .../arm/s5l8702/ipodnano3g/power-nano3g.c | 14 +++- .../arm/s5l8702/ipodnano3g/rtc-nano3g.c | 27 +++----- .../arm/s5l8702/ipodnano3g/wmcodec-nano3g.c | 65 ++++++++++++++++++ firmware/target/arm/s5l8702/pcm-s5l8702.c | 29 ++++++-- 18 files changed, 286 insertions(+), 129 deletions(-) delete mode 100644 firmware/target/arm/s5l8702/ipodnano3g/cscodec-nano3g.c create mode 100644 firmware/target/arm/s5l8702/ipodnano3g/wmcodec-nano3g.c diff --git a/apps/settings_list.c b/apps/settings_list.c index 24edac9bb7..17f22dfd11 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -426,7 +426,7 @@ static const char graphic_numeric[] = "graphic,numeric"; # define DEFAULT_REC_MIC_GAIN 0 # define DEFAULT_REC_LEFT_GAIN 0 # define DEFAULT_REC_RIGHT_GAIN 0 -# elif defined(HAVE_WM8975) +# elif defined(HAVE_WM8975) || defined(HAVE_WM1870) # define DEFAULT_REC_MIC_GAIN 16 # define DEFAULT_REC_LEFT_GAIN 0 # define DEFAULT_REC_RIGHT_GAIN 0 diff --git a/firmware/SOURCES b/firmware/SOURCES index 0c36888baa..39326ccb9a 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -575,7 +575,7 @@ drivers/audio/wm8740.c drivers/audio/wm8751.c #elif defined(HAVE_WM8978) drivers/audio/wm8978.c -#elif defined(HAVE_WM8975) +#elif defined(HAVE_WM8975) || defined(HAVE_WM1870) drivers/audio/wm8975.c #elif defined(HAVE_WM8985) drivers/audio/wm8985.c @@ -1647,7 +1647,7 @@ target/arm/s5l8702/ipodnano3g/serial-nano3g.c #endif #ifndef BOOTLOADER target/arm/s5l8702/ipodnano3g/audio-nano3g.c -target/arm/s5l8702/ipodnano3g/cscodec-nano3g.c // TODO: -> wmcodec-nano3g.c +target/arm/s5l8702/ipodnano3g/wmcodec-nano3g.c #endif #endif /* IPOD_NANO3G */ diff --git a/firmware/drivers/audio/wm8975.c b/firmware/drivers/audio/wm8975.c index 46128cc79d..848d972f4c 100644 --- a/firmware/drivers/audio/wm8975.c +++ b/firmware/drivers/audio/wm8975.c @@ -25,6 +25,12 @@ * ****************************************************************************/ +/* + * Wolfson WM8975 codec driver. It also covers the WM1870 fitted to the iPod + * Nano 3G (HAVE_WM1870), which answers the same registers and adds a few + * above them; the differences are behind that define. + */ + #include "logf.h" #include "system.h" #include "kernel.h" @@ -34,6 +40,9 @@ #include "wmcodec.h" #include "audiohw.h" +#ifdef HAVE_WM1870 +#include "pcm_sampr.h" +#endif static unsigned short wm8975_regs[WM8975_NUM_REGISTERS] = { @@ -126,7 +135,35 @@ void audiohw_postinit(void) wm8975_write(PWRMGMT1, wm8975_regs[PWRMGMT1]); audiohw_mute(false); } -#else /* !IPOD_NANO2G */ +#elif defined(HAVE_WM1870) +/* The WM1870, set up as the original firmware does it. It answers the + * WM8975's registers and a few above them. */ + +void audiohw_preinit(void) +{ + wm8975_write(RESET, RESET_RESET); + + wm8975_write(PWRMGMT1, PWRMGMT1_VMIDSEL_50K | PWRMGMT1_VREF); + wm8975_write(AINTFCE, AINTFCE_MS | AINTFCE_LRP_I2S_RLO + | AINTFCE_IWL_16BIT | AINTFCE_FORMAT_I2S); + wm8975_write(PWRMGMT2, PWRMGMT2_DACL | PWRMGMT2_DACR + | PWRMGMT2_LOUT1 | PWRMGMT2_ROUT1); + + wm8975_write(LOUTMIX1, LOUTMIX1_LD2LO | LOUTMIX1_LI2LOVOL(2) + | LOUTMIX1_LMIXSEL_LADCIN); + wm8975_write(ROUTMIX2, ROUTMIX2_RD2RO | ROUTMIX2_RI2ROVOL(2)); + wm8975_write(ROUTMIX1, ROUTMIX1_RMIXSEL_RADCIN); + + wmcodec_write(WM1870_R67, 0x008); + wm8975_write(ADDCTRL2, ADDCTRL2_OUT3SW_MONOOUT | ADDCTRL2_LRCM); + wm8975_write(ADDCTRL3, ADDCTRL3_VROI); +} + +void audiohw_postinit(void) +{ + audiohw_mute(false); +} +#else /* !IPOD_NANO2G && !HAVE_WM1870 */ void audiohw_preinit(void) { /* POWER UP SEQUENCE */ @@ -240,7 +277,15 @@ void audiohw_close(void) /* Note: Disable output before calling this function */ void audiohw_set_frequency(int fsel) { +#ifdef HAVE_WM1870 + /* The rate codes depend on the MCLK, so the target supplies them */ + unsigned short sampctrl = wmcodec_sampctrl(hw_freq_sampr[fsel]); + + if (sampctrl) + wm8975_write(SAMPCTRL, sampctrl); +#else (void)fsel; +#endif } #ifdef HAVE_RECORDING diff --git a/firmware/export/audiohw.h b/firmware/export/audiohw.h index 6525cc0452..4b68144abb 100644 --- a/firmware/export/audiohw.h +++ b/firmware/export/audiohw.h @@ -169,7 +169,7 @@ struct sound_settings_info #include "wm8751.h" #elif defined(HAVE_WM8978) #include "wm8978.h" -#elif defined(HAVE_WM8975) +#elif defined(HAVE_WM8975) || defined(HAVE_WM1870) #include "wm8975.h" #elif defined(HAVE_WM8985) #include "wm8985.h" diff --git a/firmware/export/config/ipodnano3g.h b/firmware/export/config/ipodnano3g.h index 6bcb05f2e3..5e1b872e3a 100644 --- a/firmware/export/config/ipodnano3g.h +++ b/firmware/export/config/ipodnano3g.h @@ -138,22 +138,17 @@ #define HAVE_MORSE_INPUT /* define this if you have a real-time clock */ -// TODO -//#define CONFIG_RTC RTC_NANO3G -#define CONFIG_RTC 0 +#define CONFIG_RTC RTC_NANO3G /* Define if the device can wake from an RTC alarm */ //#define HAVE_RTC_ALARM #define CONFIG_LCD LCD_IPOD6GNANO3G4G -// TODO -#if 0 -/* Define the type of audio codec */ -#define HAVE_WM8975 -#endif -// XXX: dummy for preliminary build, WRONG CODEC!!! -#define HAVE_CS42L55 +/* Define the type of audio codec. The Nano 3G has a Wolfson WM1870 at I2C + * address 0x34, which the original firmware drives as a WM8975 plus a few + * registers above the WM8975's range, so wm8975.c covers it. */ +#define HAVE_WM1870 #define HAVE_PCM_DMA_ADDRESS diff --git a/firmware/export/wm8975.h b/firmware/export/wm8975.h index 8e5e109398..354f798033 100644 --- a/firmware/export/wm8975.h +++ b/firmware/export/wm8975.h @@ -313,4 +313,7 @@ void audiohw_enable_lineout(bool enable); #define WM8975_88200HZ 0x7f #define WM8975_96000HZ 0x5d +/* The WM1870 (HAVE_WM1870) answers these registers and a few above them */ +#define WM1870_R67 0x43 + #endif /* _WM8975_H */ diff --git a/firmware/export/wmcodec.h b/firmware/export/wmcodec.h index f57933eb51..d2938eb1be 100644 --- a/firmware/export/wmcodec.h +++ b/firmware/export/wmcodec.h @@ -20,3 +20,9 @@ void wmcodec_write(int reg, int data); +#ifdef HAVE_WM1870 +/* SAMPCTRL for a sample rate, which depends on the MCLK the target feeds + * the codec. Zero if the target cannot make that rate. */ +unsigned short wmcodec_sampctrl(unsigned long rate); +#endif + diff --git a/firmware/target/arm/ipod/button-clickwheel.c b/firmware/target/arm/ipod/button-clickwheel.c index fc10efc9a4..1f4ca13dc8 100644 --- a/firmware/target/arm/ipod/button-clickwheel.c +++ b/firmware/target/arm/ipod/button-clickwheel.c @@ -425,8 +425,13 @@ bool headphones_inserted(void) #if CONFIG_CPU==S5L8701 return ((PDAT14 & (1 << 5)) != 0); #elif CONFIG_CPU==S5L8702 || CONFIG_CPU==S5L8720 +#ifdef IPOD_NANO3G + /* GPIO 7.0, high with headphones in (measured) */ + return (PDAT(7) & (1 << 0)) != 0; +#else return ((PDAT10 & (1 << 6)) != 0); #endif +#endif } #endif diff --git a/firmware/target/arm/s5l8702/ipodnano3g/TESTING.md b/firmware/target/arm/s5l8702/ipodnano3g/TESTING.md index 88702883a4..bf26391dbc 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/TESTING.md +++ b/firmware/target/arm/s5l8702/ipodnano3g/TESTING.md @@ -21,3 +21,13 @@ Tested on the 4GB unit from DFU: host sees "Nano 3G NAND", 2113537 x 2048-byte sectors, write protect on. `nandcheck.py collect` reads it in seven minutes on Linux and Windows, no ECC failures or timeouts, and the archive passes the host FTL suite. + +## Power, RTC, backlight, battery, audio + +Tested on the 4GB unit: 44.1 kHz WAV plays with elapsed time running; +battery reads 4077 mV, charging true/false correctly with a cell below +full; a time set in Rockbox survives a reboot; backlight and brightness +work; "pause on headphone unplug" pauses on disconnect. + +Not tested: sample rates other than 44.1 kHz, and the charge-complete +status bits (never seen set). diff --git a/firmware/target/arm/s5l8702/ipodnano3g/adc-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/adc-nano3g.c index c1e63e76f0..794dc07aa1 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/adc-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/adc-nano3g.c @@ -27,17 +27,51 @@ #include "pmu-target.h" #include "kernel.h" +/* Only the battery input is known: the original firmware selects it with + * 0x24 and converts with 2500 mV + raw * 2000 mV / 1023 (measured: 4062 mV + * on battery, 4110 mV charging). The USB data and accessory inputs are not + * identified yet and read as 0. */ +static const struct pmu_adc_channel adc_channels[] = +{ + [ADC_BATTERY] = + { + .name = "Battery", + .mux = 0x24, + .samples = 4, + .offset_mv = 2500, + .span_mv = 2000, + }, + [ADC_USBDATA] = + { + .name = "USB data", + }, + [ADC_ACCESSORY] = + { + .name = "Accessory", + }, +}; + +unsigned short adc_read_millivolts(int channel) +{ + const struct pmu_adc_channel *ch = &adc_channels[channel]; + + if (!ch->samples) + return 0; + return pmu_adc_raw2mv(ch, pmu_read_adc(ch)); +} + /* Returns battery voltage [millivolts] */ unsigned int adc_read_battery_voltage(void) { - return 0; + return adc_read_millivolts(ADC_BATTERY); } /* API functions */ unsigned short adc_read(int channel) { - (void) channel; - return 0; + const struct pmu_adc_channel *ch = &adc_channels[channel]; + + return ch->samples ? pmu_read_adc(ch) : 0; } int adc_read_accessory_resistor(void) @@ -53,8 +87,7 @@ unsigned int adc_read_usbdata_voltage(bool dp) const char *adc_name(int channel) { - (void) channel; - return ""; + return adc_channels[channel].name; } void adc_init(void) diff --git a/firmware/target/arm/s5l8702/ipodnano3g/backlight-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/backlight-nano3g.c index 34fc198471..5b557aabd5 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/backlight-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/backlight-nano3g.c @@ -30,7 +30,7 @@ #include "lcd-s5l8702.h" #endif -// TODO: test +/* The original firmware writes the level to bits 0..4 as well */ void backlight_hw_brightness(int brightness) { pmu_write(D1671_REG_LEDCTL, @@ -43,8 +43,11 @@ void backlight_hw_on(void) if (!lcd_active()) lcd_awake(); #endif + /* As the original firmware turns it on: bits 7 and 6 set, + * bit 5 clear. Bit 6's meaning is unknown. */ pmu_write(D1671_REG_LEDCTL, - (pmu_read(D1671_REG_LEDCTL) | D1671_LEDCTL_ENABLE)); + (pmu_read(D1671_REG_LEDCTL) & ~0x20) + | D1671_LEDCTL_ENABLE | D1671_LEDCTL_UNKNOWN); } void backlight_hw_off(void) diff --git a/firmware/target/arm/s5l8702/ipodnano3g/cscodec-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/cscodec-nano3g.c deleted file mode 100644 index e3ebfc9525..0000000000 --- a/firmware/target/arm/s5l8702/ipodnano3g/cscodec-nano3g.c +++ /dev/null @@ -1,67 +0,0 @@ -/*************************************************************************** - * __________ __ ___. - * Open \______ \ ____ ____ | | _\_ |__ _______ ___ - * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / - * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < - * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ - * \/ \/ \/ \/ \/ - * - * S5L8702-specific code for Cirrus codecs - * - * Copyright (c) 2010 Michael Sparmann - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - ****************************************************************************/ - -#include "system.h" -#include "audiohw.h" -#include "i2c-s5l8702.h" -#include "s5l87xx.h" -#include "cscodec.h" - -void audiohw_init(void) -{ -#ifdef HAVE_CS42L55 - audiohw_preinit(); -#endif -} - -unsigned char cscodec_read(int reg) -{ - // TODO: not tested - unsigned char data; - i2c_read(0, 0x94, reg, 1, &data); - return data; -} - -void cscodec_write(int reg, unsigned char data) -{ - // XXX: not tested - i2c_write(0, 0x94, reg, 1, &data); -} - -void cscodec_power(bool state) -{ - (void)state; //TODO: Figure out which LDO this is -} - -void cscodec_reset(bool state) -{ - // XXX: not tested - if (state) PDAT(3) &= ~8; - else PDAT(3) |= 8; -} - -void cscodec_clock(bool state) -{ - // XXX: not tested - if (state) CLKCON3 &= ~0xffff; - else CLKCON3 |= 0x8000; -} diff --git a/firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c index f3a6aa521a..e688e01402 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/pmu-nano3g.c @@ -112,16 +112,26 @@ void pmu_set_cpu_voltage(bool high) #endif #if (CONFIG_RTC == RTC_NANO3G) +/* The RTC, as the original firmware uses it: + * registers 0x40..0x45 hold seconds, minutes, hours, day, month and year + * (since 2000), in binary. Bit 6 of the seconds register is set whenever + * the time is written and masked off when it is read. The buffer holds the + * six values in that order. */ void pmu_read_rtc(unsigned char* buffer) { - // TODO - (void) buffer; + pmu_read_multiple(D1671_REG_RTCSEC, 6, buffer); + buffer[0] &= ~D1671_RTCSEC_SET; } void pmu_write_rtc(unsigned char* buffer) { - // TODO - (void) buffer; + int i; + + /* One register at a time, seconds first, as the original firmware + * does */ + for (i = 0; i < 6; i++) + pmu_write(D1671_REG_RTCSEC + i, + buffer[i] | (i == 0 ? D1671_RTCSEC_SET : 0)); } #endif @@ -132,7 +142,10 @@ void pmu_set_usblimit(bool fast_charge) } /* - * ADC + * ADC, as the original firmware drives it: write the channel's + * input selection to register 0x30 with bit 3 set to start a conversion, + * wait for bit 3 to clear, and read the 10-bit result from 0x32 (bits 9..2) + * and 0x31 (bits 1..0). 0x30 idles at 0x20. */ static struct mutex pmu_adc_mutex; @@ -140,21 +153,31 @@ static struct mutex pmu_adc_mutex; unsigned short pmu_adc_raw2mv( const struct pmu_adc_channel *ch, unsigned short raw) { - // TODO - (void) ch; - (void) raw; - return 0; + return ch->offset_mv + raw * ch->span_mv / 1023; } -/* returns raw value */ +/* returns raw value, averaged over ch->samples conversions */ unsigned short pmu_read_adc(const struct pmu_adc_channel *ch) { - // TODO - int raw = 0; + unsigned int sum = 0; + int i, tries; + mutex_lock(&pmu_adc_mutex); - (void) ch; + for (i = 0; i < ch->samples; i++) + { + pmu_write(D1671_REG_ADCCTL, ch->mux | D1671_ADCCTL_START); + for (tries = 0; tries < 20; tries++) + { + udelay(50); + if (!(pmu_read(D1671_REG_ADCCTL) & D1671_ADCCTL_START)) + break; + } + sum += (pmu_read(D1671_REG_ADCHI) << 2) + | (pmu_read(D1671_REG_ADCLO) & 3); + } + pmu_write(D1671_REG_ADCCTL, D1671_ADCCTL_IDLE); mutex_unlock(&pmu_adc_mutex); - return raw; + return ch->samples ? sum / ch->samples : 0; } /* diff --git a/firmware/target/arm/s5l8702/ipodnano3g/pmu-target.h b/firmware/target/arm/s5l8702/ipodnano3g/pmu-target.h index 5278102ffa..f8a50ef8e5 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/pmu-target.h +++ b/firmware/target/arm/s5l8702/ipodnano3g/pmu-target.h @@ -37,6 +37,9 @@ enum d1671_regs { D1671_REG_SYSCTRLA = 0x08, D1671_REG_LEDCTL = 0x20, D1671_REG_CHCTL = 0x21, // TBC + D1671_REG_ADCCTL = 0x30, + D1671_REG_ADCLO = 0x31, + D1671_REG_ADCHI = 0x32, D1671_REG_RTCSEC = 0x40, // TBC }; @@ -87,13 +90,26 @@ enum d1671_reg_chctl { D1671_CHCTL_FASTCHRG = 0x01, /* 100/500mA USB limit */ }; +enum d1671_reg_rtcsec { + /* set when the time is written, masked off when it is read */ + D1671_RTCSEC_SET = 0x40, +}; + +enum d1671_reg_adcctl { + D1671_ADCCTL_START = 0x08, + D1671_ADCCTL_IDLE = 0x20, /* register's value at rest */ +}; + /* GPIO for external PMU interrupt */ #define GPIO_EINT_PMU 0x7b struct pmu_adc_channel { const char *name; - // TODO + uint8_t mux; /* register 0x30 value selecting the input */ + uint8_t samples; /* conversions averaged, 0 = not readable */ + unsigned short offset_mv; /* millivolts at raw 0 */ + unsigned short span_mv; /* millivolts from raw 0 to raw 1023 */ }; void pmu_preinit(void); diff --git a/firmware/target/arm/s5l8702/ipodnano3g/power-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/power-nano3g.c index 8a957aa89a..e4e8aad8bb 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/power-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/power-nano3g.c @@ -58,9 +58,19 @@ unsigned int power_input_status(void) return status; } +/* As the original firmware decides it: the charger is + * enabled (CHCTL bits 1..6), not suspended (SYSCTRLA bit 2), and not done: + * STATUSB bits 1..2 clear. Measured: they read 0 while a 4.1 V cell + * charges from USB, so they are taken as the charge-complete indication; + * they have not been seen set. */ bool charging_state(void) { - // TODO - return false; + if (!(power_input_status() & POWER_INPUT_CHARGER)) + return false; + if (!(pmu_read(D1671_REG_CHCTL) & 0x7e)) + return false; + if (pmu_read(D1671_REG_SYSCTRLA) & 0x04) + return false; + return !(pmu_read(D1671_REG_STATUSB) & 0x06); } #endif /* CONFIG_CHARGING */ diff --git a/firmware/target/arm/s5l8702/ipodnano3g/rtc-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/rtc-nano3g.c index 215689c725..6f79e278e4 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/rtc-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/rtc-nano3g.c @@ -24,6 +24,8 @@ #include "pmu-target.h" #include "timefuncs.h" +/* The PMU keeps seconds, minutes, hours, day, month and year (since 2000) + * in binary, and no weekday; see pmu_read_rtc() */ void rtc_init(void) { @@ -31,20 +33,16 @@ void rtc_init(void) int rtc_read_datetime(struct tm *tm) { - unsigned int i; - unsigned char buf[7]; + unsigned char buf[6]; pmu_read_rtc(buf); - for (i = 0; i < sizeof(buf); i++) - buf[i] = BCD2DEC(buf[i]); - tm->tm_sec = buf[0]; tm->tm_min = buf[1]; tm->tm_hour = buf[2]; - tm->tm_mday = buf[4]; - tm->tm_mon = buf[5] - 1; - tm->tm_year = buf[6] + 100; + tm->tm_mday = buf[3]; + tm->tm_mon = buf[4] - 1; + tm->tm_year = buf[5] + 100; tm->tm_yday = 0; /* Not implemented for now */ set_day_of_week(tm); @@ -53,19 +51,14 @@ int rtc_read_datetime(struct tm *tm) int rtc_write_datetime(const struct tm *tm) { - unsigned int i; - unsigned char buf[7]; + unsigned char buf[6]; buf[0] = tm->tm_sec; buf[1] = tm->tm_min; buf[2] = tm->tm_hour; - buf[3] = tm->tm_wday; - buf[4] = tm->tm_mday; - buf[5] = tm->tm_mon + 1; - buf[6] = tm->tm_year - 100; - - for (i = 0; i < sizeof(buf); i++) - buf[i] = DEC2BCD(buf[i]); + buf[3] = tm->tm_mday; + buf[4] = tm->tm_mon + 1; + buf[5] = tm->tm_year - 100; pmu_write_rtc(buf); return 0; diff --git a/firmware/target/arm/s5l8702/ipodnano3g/wmcodec-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/wmcodec-nano3g.c new file mode 100644 index 0000000000..2043bfb486 --- /dev/null +++ b/firmware/target/arm/s5l8702/ipodnano3g/wmcodec-nano3g.c @@ -0,0 +1,65 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * iPod Nano 3G code for the Wolfson codec (WM1870) + * + * Copyright (c) 2010 Michael Sparmann + * Copyright (C) 2026 by Andrew Rice + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "system.h" +#include "audiohw.h" +#include "i2c-s5l8702.h" +#include "wmcodec.h" + +/* The PCM driver sets the codec up (audiohw_preinit()) and controls its + * MCLK */ +void audiohw_init(void) +{ +} + +/* The codec runs from a 12 MHz MCLK here, so its rate codes are the USB + * mode ones, with a BCLK divider, as the original firmware sets them. */ +unsigned short wmcodec_sampctrl(unsigned long rate) +{ + static const struct + { + unsigned long rate; + unsigned short sampctrl; + } rates[] = + { + { SAMPR_8, 0x10d }, { SAMPR_11, 0x133 }, { SAMPR_12, 0x111 }, + { SAMPR_16, 0x115 }, { SAMPR_22, 0x137 }, { SAMPR_24, 0x139 }, + { SAMPR_32, 0x119 }, { SAMPR_44, 0x123 }, { SAMPR_48, 0x081 }, + { SAMPR_88, 0x0bf }, { SAMPR_96, 0x09d }, + }; + unsigned int i; + + for (i = 0; i < ARRAYLEN(rates); i++) + if (rates[i].rate == rate) + return rates[i].sampctrl; + return 0; +} + +/* 7-bit register and 9-bit value, as two bytes to address 0x34 on I2C + * bus 0. The codec does not acknowledge reads. */ +void wmcodec_write(int reg, int data) +{ + unsigned char d = data & 0xff; + + i2c_write(0, 0x34, (reg << 1) | ((data >> 8) & 1), 1, &d); +} diff --git a/firmware/target/arm/s5l8702/pcm-s5l8702.c b/firmware/target/arm/s5l8702/pcm-s5l8702.c index 9af30b9c4b..57ad20f7c4 100644 --- a/firmware/target/arm/s5l8702/pcm-s5l8702.c +++ b/firmware/target/arm/s5l8702/pcm-s5l8702.c @@ -80,6 +80,18 @@ static struct dmac_ch_cfg dma_play_ch_cfg = { #define CHUNK_MAX_BYTES (LLI_MAX_BYTES * 1) #define WATERMARK_BYTES (PCM_WATERMARK * 4) +/* I2S0 TX setup and start command. The Nano 3G uses the values Apple's + * firmware writes for its WM1870, which is the I2S clock master; the + * ipod6g values have TXCON bits 3-4 and TXCOM bit 3 set as well, and with + * them I2S0 never takes the codec's BCLK/LRCK. */ +#ifdef IPOD_NANO3G +#define I2STXCON_SETUP 0x0b100001 +#define I2STXCOM_START 0x6 +#else +#define I2STXCON_SETUP 0xb100019 +#define I2STXCOM_START 0xe +#endif + static volatile int locked = 0; static unsigned char dblbuf[2][WATERMARK_BYTES] CACHEALIGN_ATTR; static int active_dblbuf; @@ -152,7 +164,7 @@ static void sink_dma_start(const void* addr, size_t size) sink_dma_stop(); pcm_remaining = size; - I2STXCOM = 0xe; + I2STXCOM = I2STXCOM_START; dma_play_callback((void*)addr); } @@ -162,9 +174,16 @@ static void sink_dma_start(const void* addr, size_t size) /* set the configured PCM frequency */ static void sink_set_freq(uint16_t freq) { +#ifdef HAVE_CS42L55 static uint16_t last_clkcon3l = 0; - uint16_t clkcon3l; +#else + /* Not a valid setting, so the first call always programs MCLK. The + * CS42L55 driver starts MCLK itself; the WM8975 driver does not. */ + static uint16_t last_clkcon3l = 0xffff; +#endif + uint16_t clkcon3l = 0; /* OSC0 -> 12 MHz */ +#ifdef HAVE_CS42L55 /* For unknown reasons, s5l8702 I2S controller does not synchronize * with CS42L55 at 32000 Hz. To fix it, the CODEC is configured with * a sample rate of 48000 Hz and MCLK is decreased 1/3 to 8 Mhz, @@ -175,9 +194,7 @@ static void sink_set_freq(uint16_t freq) freq = HW_FREQ_48; clkcon3l = 0x3028; /* PLL2 / 3 / 9 -> 8 MHz */ } - else { - clkcon3l = 0; /* OSC0 -> 12 MHz */ - } +#endif /* configure MCLK */ /* TODO: maybe all CLKCON management should be moved to @@ -201,7 +218,7 @@ static void sink_dma_init(void) dmac_ch_init(&dma_play_ch, &dma_play_ch_cfg); - I2STXCON = 0xb100019; + I2STXCON = I2STXCON_SETUP; I2SCLKCON = 1; audiohw_preinit(); From 1ad17c9b57cdba6998cc136c41fc2ea7c3d12407 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Sat, 19 Sep 2026 07:52:27 +1000 Subject: [PATCH 189/408] manual: add the iPod Nano 3G Adds the platform file, modelled on the Nano 2G's and the iPod Classic's, and corrects the supported-versions text shared by the Nano manuals, which said Rockbox does not run on the third generation. Co-authored-by: Claude Opus 5 Change-Id: Ie6290d67ae113086cb8a63c825eb9db9544c1d49 --- manual/getting_started/installation.tex | 34 ++++++++++++---- manual/getting_started/ipodnano3g_install.tex | 40 +++++++++++++++++++ manual/platform/ipodnano3g.tex | 24 +++++++++++ manual/rockbox_interface/main.tex | 2 +- 4 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 manual/getting_started/ipodnano3g_install.tex create mode 100644 manual/platform/ipodnano3g.tex diff --git a/manual/getting_started/installation.tex b/manual/getting_started/installation.tex index a11ed99cef..22ada06789 100644 --- a/manual/getting_started/installation.tex +++ b/manual/getting_started/installation.tex @@ -10,17 +10,27 @@ before beginning there are a few important things to know. follow the instructions at \wikilink{SansaE200RInstallation}.}} -\opt{ipodnano,ipodnano2g,ipodvideo,ipod6g,e200,c200,c200v2,e200v2,clipv1,clipv2,cowond2,fuze,fuzev2}{ +\opt{ipodnano,ipodnano2g,ipodnano3g,ipodvideo,ipod6g,e200,c200,c200v2,e200v2,clipv1,clipv2,cowond2,fuze,fuzev2}{ \begin{description} \item[Supported hardware versions.] - \opt{ipodnano,ipodnano2g}{ + \opt{ipodnano,ipodnano2g,ipodnano3g}{ The \playertype{} is available in multiple versions, not - all of which run Rockbox. Rockbox presently runs only on - the first and second generation iPod Nano. Rockbox does - \emph{not} run on the third, fourth or fifth generation iPod Nano. + all of which run Rockbox. Rockbox presently runs on the first, + second and third generation iPod Nano. Rockbox does + \emph{not} run on the fourth, fifth, sixth or seventh generation + iPod Nano. For information on identifying which iPod you own, see this page on Apple's website: \url{http://www.info.apple.com/kbnum/n61688}. } + \opt{ipodnano3g}{ + The \playertype{} referred to by this manual is the third generation + model, the short and wide one with a \dapdisplaywidth{}x% + \dapdisplayheight{} screen. It came with 4GB and 8GB of flash storage. + Apple fitted several different NAND chips, and Rockbox so far supports + the models with Hynix NAND, 4GB and 8GB: on any other chip the + bootloader says so and starts Apple's firmware instead. The + installation section below says how to have your player's chip added. + } \opt{ipodvideo}{ The \playertype{} is the 5th/5.5th generation \playerman{} only. For information on identifying which iPod you own, see this page on Apple's @@ -349,7 +359,7 @@ It is also worth noting that data transfer rates are not likely to be improved w \end{itemize} } -\opt{gigabeats,fiiom3k,shanlingq1}{ +\opt{gigabeats,fiiom3k,shanlingq1,ipodnano3g}{ \note{The automated install is not yet available for the \playerlongtype{}. For now you can use the manual method to install Rockbox. Please still read the section on the automatic install as it explains @@ -674,10 +684,12 @@ by Rockbox, in the main directory of your \daps{} drive. \opt{iriverh100,iriverh300}{\input{getting_started/iriver_install.tex}} \opt{mpiohd200,mpiohd300}{\input{getting_started/mpio_install.tex}} \opt{ipod}{ - \nopt{ipod6g} { + \nopt{ipod6g,ipodnano3g} { \input{getting_started/ipod_install.tex}} \opt{ipod6g} { - \input{getting_started/ipod6g_install.tex}} } + \input{getting_started/ipod6g_install.tex}} + \opt{ipodnano3g} { + \input{getting_started/ipodnano3g_install.tex}} } \opt{iaudio}{\input{getting_started/iaudio_install.tex}} \opt{iriverh10,iriverh10_5gb}{\input{getting_started/h10_install.tex}} \opt{gigabeatf}{\input{getting_started/gigabeat_install.tex}} @@ -934,10 +946,16 @@ completely follow the manual uninstallation instructions below.}} } \optv{ipod}{ + \nopt{ipodnano3g}{ To uninstall Rockbox and go back to using just the original iPod software, connect the \dap{} to your computer and follow the instructions to install the bootloader but, when prompted by ipodpatcher, enter \texttt{u} for uninstall instead of \texttt{i} for install. + } + \opt{ipodnano3g}{ + Follow the DFU-mode steps above, then run + \fname{mks5lboot --bl-uninst ipodnano3g} to remove the bootloader. + } } \opt{iaudiom3,iaudiom5,iaudiox5}{ diff --git a/manual/getting_started/ipodnano3g_install.tex b/manual/getting_started/ipodnano3g_install.tex new file mode 100644 index 0000000000..3f4447b233 --- /dev/null +++ b/manual/getting_started/ipodnano3g_install.tex @@ -0,0 +1,40 @@ +The third generation iPod Nano uses a DFU installer, \fname{mks5lboot}, +to install the bootloader in flash. Do not use \fname{ipodpatcher}. + +\warn{Rockbox supports the Nano 3G models fitted with Hynix NAND, the +4~GB A514D3AD and the 8~GB A555D5AD. Apple fitted several other NAND +chips to this player. Rockbox drives only chips that have been tested on +hardware: on any other chip the bootloader says the NAND is not supported +and starts Apple's firmware instead, leaving the player's storage +untouched. The port is experimental: Apple's proactive +refresh of blocks needing ECC correction is not implemented, and +bad-block remapping has not been exercised on hardware. Keep a backup of +all media.} + +If your player has another chip, you can help have it added. The NAND check in \fname{utils/ipodnano3g/nandcheck} of the +source tree runs from DFU mode, installs nothing, writes nothing to the +player, and collects what validating the chip needs: see that +directory's \fname{README.md}. + +\caps{Rockbox Utility} can install the bootloader. To install it by hand +instead, follow the steps below. + +Use \fname{mks5lboot} built from Rockbox sources +with Nano 3G support, and the Nano 3G bootloader +\fname{bootloader-ipodnano3g.ipod}. Older iPod Classic installers do not +support this player. Build instructions and USB driver requirements are +in \fname{utils/mks5lboot/README} in the source tree. + +\begin{enumerate} +\item Install the Rockbox files on the player's FAT32 volume before +installing the bootloader. +\item Connect the player by USB. Close iTunes and stop iTunesHelper if +they are running. +\item Run \fname{mks5lboot --dfuscan --loop} in a terminal, then put the +player in +\href{https://freemyipod.org/wiki/Modes#DFU_mode}{DFU mode}. +When it is detected, stop the scan with Ctrl+C. +\item Run \fname{mks5lboot --bl-inst path/to/bootloader-ipodnano3g.ipod}, +replacing the path with the location of your Nano 3G bootloader. +\item Wait for the installer to finish and the player to restart. +\end{enumerate} diff --git a/manual/platform/ipodnano3g.tex b/manual/platform/ipodnano3g.tex new file mode 100644 index 0000000000..8a4b257aab --- /dev/null +++ b/manual/platform/ipodnano3g.tex @@ -0,0 +1,24 @@ +\def\UseOption{ipodnano3g} +\edef\UseOption{\UseOption,HAVE_RB_BL_IN_FLASH} +\edef\UseOption{\UseOption,IPOD_4G_PAD} +\edef\UseOption{\UseOption,HAVE_BACKLIGHT} +\edef\UseOption{\UseOption,ipod} + +\newcommand{\playerman}{Ipod} +\newcommand{\playertype}{Nano} +\newcommand{\playerlongtype}{\playertype{} 3rd generation} +\newcommand{\dapdisplaywidth}{320} +\newcommand{\dapdisplayheight}{240} +\newcommand{\dapdisplaydepth}{16} +\newcommand{\specimg}{ipodnano3g} +%Used to name the player, e.g. ...to the \dap +\newcommand{\dap}{player} +%For use when referring to the player. E.g. the \daps\ capacity ... +\newcommand{\daps}{player's} +\newcommand{\firmwarefilename}{\fname{rockbox.ipod}} +\newcommand{\firmwareextension}{\fname{ipod}} +\newcommand{\screenshotsize}{8cm} +\newcommand{\disk}{flash storage} + +% link external keymap file +\input{platform/keymap-ipod4g.tex} diff --git a/manual/rockbox_interface/main.tex b/manual/rockbox_interface/main.tex index 594e261401..8d6f4ebd1a 100644 --- a/manual/rockbox_interface/main.tex +++ b/manual/rockbox_interface/main.tex @@ -630,7 +630,7 @@ To turn on and off your Rockbox enabled \dap{} use the following keys: { \subsection{Starting the original firmware} \label{ref:Dualboot} - \opt{ipod4g,ipodcolor,ipodvideo,ipodnano,ipodnano2g,ipodmini} + \opt{ipod4g,ipodcolor,ipodvideo,ipodnano,ipodnano2g,ipodnano3g,ipodmini} { Rockbox has a dual-boot feature. To boot into the original firmware, shut down the device as described above. Turn on the \ButtonHold{} switch From 6719578e29b874d1956ca944eb5b439c845abbbe Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Sun, 20 Sep 2026 03:54:13 +0300 Subject: [PATCH 190/408] rbutilqt/themeeditor: Fix Apple code signing issues - The main binary in *.app/Contents/MacOS/ was not signed using the "hardened runtime" option. - The ipodpatcher and sansapatcher binaries in RockboxUtility.app/Contents/bin/ were not signed at all. The produced .dmg images are now suitable for notarization so the app inside can be installed and started with no security warnings. They are not notarized yet, as this takes a significant amount of time, but it can be done manually using: xcrun notarytool submit build-qt/RockboxUtility.dmg --keychain-profile --wait xcrun stapler staple build-qt/RockboxUtility.dmg Only ARM macOS is supported at the moment. Co-authored-by: ChatGPT-5.6 Luna Change-Id: Id88d7da18f541f9f503172f5dcb5b17d64e0602e --- utils/CMakeLists.txt | 16 ++++++++++++++++ utils/cmake/deploy.cmake | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index fc771a093d..06aff2195b 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -133,6 +133,13 @@ target_compile_definitions(ipodpatcher-bin PRIVATE VERSION="none") set_target_properties(ipodpatcher-bin PROPERTIES OUTPUT_NAME ipodpatcher) if(APPLE) target_link_libraries(ipodpatcher-bin ${FRAMEWORK_IOKIT} ${FRAMEWORK_COREFOUNDATION}) + + add_custom_command(TARGET ipodpatcher-bin POST_BUILD + COMMAND codesign --force --options runtime + --sign "${CODESIGN_IDENTITY}" + "$" + VERBATIM + ) endif() add_library(ipodpatcher @@ -164,6 +171,15 @@ target_link_libraries(sansapatcher-bin sansapatcher) target_compile_definitions(sansapatcher-bin PRIVATE VERSION="none") set_target_properties(sansapatcher-bin PROPERTIES OUTPUT_NAME sansapatcher) +if(APPLE) + add_custom_command(TARGET sansapatcher-bin POST_BUILD + COMMAND codesign --force --options runtime + --sign "${CODESIGN_IDENTITY}" + "$" + VERBATIM + ) +endif() + add_library(sansapatcher sansapatcher/parttypes.h sansapatcher/sansaio.h diff --git a/utils/cmake/deploy.cmake b/utils/cmake/deploy.cmake index cfbae24d50..7d4da8f3be 100644 --- a/utils/cmake/deploy.cmake +++ b/utils/cmake/deploy.cmake @@ -153,7 +153,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") COMMENT "Running macdeployqt and creating dmg ${deploy_TARGET}" COMMAND ${MACDEPLOYQT_EXECUTABLE} ${deploy_TARGET}.app - COMMAND codesign --force --deep --sign "${CODESIGN_IDENTITY}" ${deploy_TARGET}.app + COMMAND codesign --force --options runtime --deep --sign "${CODESIGN_IDENTITY}" ${deploy_TARGET}.app COMMAND ${DMGBUILD} -s ${deploy_DMGBUILDCFG} -Dappbundle=${deploy_TARGET}.app From 7f01029439b59c4059f861d371d8164294f94925 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Sun, 20 Sep 2026 22:09:09 +0300 Subject: [PATCH 191/408] rbutilqt/themeeditor: Move the CODESIGN_IDENTITY variable definition before the targets which depend on it. Change-Id: I786be5b7dfaee94cf31ef9e72f43dce9700056c0 --- utils/CMakeLists.txt | 16 ++++++++++++++++ utils/cmake/deploy.cmake | 13 ------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 06aff2195b..a778035140 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -124,6 +124,22 @@ else() set(QUAZIP_LIBRARY quazip) endif() +# Setup code signing for macOS +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(CODESIGN_IDENTITY "-" CACHE STRING + "Code signing identity to use for macOS signing") + + if(DEFINED ENV{CODESIGN_IDENTITY}) + set(CODESIGN_IDENTITY "$ENV{CODESIGN_IDENTITY}") + endif() + + if("${CODESIGN_IDENTITY}" STREQUAL "-") + message("-- Using ad-hoc Apple code signing") + else() + message("-- Using Apple code signing identity: ${CODESIGN_IDENTITY}") + endif() +endif() + add_executable(ipodpatcher-bin ipodpatcher/main.c ipodpatcher/ipodpatcher_aupd.c diff --git a/utils/cmake/deploy.cmake b/utils/cmake/deploy.cmake index 7d4da8f3be..757445c67f 100644 --- a/utils/cmake/deploy.cmake +++ b/utils/cmake/deploy.cmake @@ -94,19 +94,6 @@ endif() # MacOS: Build dmg if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") - set(CODESIGN_IDENTITY "-" CACHE STRING - "Code signing identity to use for macOS signing") - - if(DEFINED ENV{CODESIGN_IDENTITY}) - set(CODESIGN_IDENTITY "$ENV{CODESIGN_IDENTITY}") - endif() - - if("${CODESIGN_IDENTITY}" STREQUAL "-") - message("-- Using ad-hoc Apple code signing") - else() - message("-- Using Apple code signing identity: ${CODESIGN_IDENTITY}") - endif() - set(DMGBUILD_STAMP ${CMAKE_BINARY_DIR}/dmgbuild.stamp) add_custom_command( COMMENT "Setting up dmgbuild virtualenv" From 93b49594d525e25544b8d098d81f179d459d0364 Mon Sep 17 00:00:00 2001 From: Paul Sauro Date: Sun, 20 Sep 2026 15:21:56 +0200 Subject: [PATCH 192/408] usb storage: fix BOT residue, rejected commands and ATA IDENTIFY Account only completed data-stage bytes in the CSW residue. Halt the requested data pipe before returning failed status for an unsupported command with a data phase. Send ATA IDENTIFY from the initialized transfer buffer rather than a stale READ/WRITE buffer pointer, and reject unsupported WRITE BUFFER variants. Change-Id: I9855f56b3555c2a69724681cb028e57f733dae0e --- firmware/usbstack/usb_storage.c | 55 ++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/firmware/usbstack/usb_storage.c b/firmware/usbstack/usb_storage.c index 6f10a087e1..0a9fa1751e 100644 --- a/firmware/usbstack/usb_storage.c +++ b/firmware/usbstack/usb_storage.c @@ -305,6 +305,7 @@ static struct { unsigned char *data[2]; unsigned char data_select; unsigned int last_result; + unsigned int data_residue; } cur_cmd; static struct { @@ -316,6 +317,8 @@ static struct { static void handle_scsi(struct command_block_wrapper* cbw); static void send_csw(int status); +static void reject_command(const struct command_block_wrapper *cbw); +static void account_data_transfer(int dir, int status, int length); static void send_command_result(void *data,int size); static void send_command_failed_result(void); static void send_block_data(void *data,int size); @@ -444,6 +447,7 @@ static int usb_storage_get_config_descriptor(unsigned char *dest,int max_packet_ #else static int usb_handle = 0; #endif + static int usb_storage_init_connection(void) { logf("ums: set config"); @@ -510,8 +514,11 @@ static void usb_storage_transfer_complete(int ep,int dir,int status,int length) struct tm tm; #endif + logf("transfer result for ep %d/%d %X %d", ep,dir,status, length); + account_data_transfer(dir, status, length); + switch(state) { case RECEIVING_BLOCKS: if(dir==USB_DIR_IN) { @@ -688,7 +695,10 @@ static void usb_storage_send_ata_identify(void) } cur_cmd.count = 0; cur_cmd.last_result = 0; - send_block_data(cur_cmd.data[0], 512); + /* IDENTIFY can be the first data command after SET_CONFIGURATION. + * cur_cmd.data[] belongs to previous READ/WRITE commands and may be + * unset, or refer to a buffer freed by a USB reset. */ + send_block_data(tb.transfer_buffer, ATA_IDENTIFY_WORDS * 2); } #ifdef HAVE_ATA_SMART static void usb_storage_send_smart(uint8_t cmd) @@ -823,6 +833,7 @@ static void handle_scsi(struct command_block_wrapper* cbw) uint32_t bsize = block_size*block_size_mult; sector_t bcount = block_count/block_size_mult; + cur_cmd.data_residue = letoh32(cbw->data_transfer_length); cur_cmd.tag = cbw->tag; cur_cmd.lun = lun; cur_cmd.cur_cmd = cbw->command_block[0]; @@ -1358,7 +1369,9 @@ static void handle_scsi(struct command_block_wrapper* cbw) && (cbw->command_block[8]==0 || cbw->command_block[8]==0x0c) && cbw->command_block[9]==0) - receive_time(); + receive_time(); + else + reject_command(cbw); break; #endif /* CONFIG_RTC */ @@ -1406,14 +1419,42 @@ static void handle_scsi(struct command_block_wrapper* cbw) #endif /* STORAGE_ATA */ default: logf("scsi unknown cmd %x",cbw->command_block[0x0]); - send_csw(UMS_STATUS_FAIL); - cur_sense_data.sense_key=SENSE_ILLEGAL_REQUEST; - cur_sense_data.asc=ASC_INVALID_COMMAND; - cur_sense_data.ascq=0; + reject_command(cbw); break; } } +/* Count data only, never the CBW or CSW. A short result must report the + * untransferred bytes instead of claiming that the whole request succeeded. */ +static void account_data_transfer(int dir, int status, int length) +{ + bool data_in = state == SENDING_RESULT || state == SENDING_FAILED_RESULT || + state == SENDING_BLOCKS; + bool data_out = state == RECEIVING_BLOCKS; +#if CONFIG_RTC + data_out |= state == RECEIVING_TIME; +#endif + if(status == 0 && length > 0 && + ((data_in && dir == USB_DIR_IN) || (data_out && dir == USB_DIR_OUT))) + cur_cmd.data_residue -= MIN(cur_cmd.data_residue, (unsigned int)length); +} + +/* BOT cases Hi/Dn and Ho/Dn still have a data phase. Sending the CSW as + * IN data makes the host wait for a second status that will never arrive. */ +static void reject_command(const struct command_block_wrapper *cbw) +{ + cur_sense_data.sense_key = SENSE_ILLEGAL_REQUEST; + cur_sense_data.asc = ASC_INVALID_COMMAND; + cur_sense_data.ascq = 0; + if(letoh32(cbw->data_transfer_length)) { + bool in = (cbw->flags & USB_DIR_IN) != 0; + /* Halt the data pipe first. The host clears it before reading CSW + * (BOT 6.7.2 case 4 / 6.7.3 case 9). Queue CSW behind that halt. */ + usb_drv_stall(in ? EP_IN : EP_OUT, true, in); + } + send_csw(UMS_STATUS_FAIL); +} + static void send_block_data(void *data,int size) { usb_drv_send_nonblocking(EP_IN, data,size); @@ -1450,7 +1491,7 @@ static void send_csw(int status) { tb.csw->signature = htole32(CSW_SIGNATURE); tb.csw->tag = cur_cmd.tag; - tb.csw->data_residue = 0; + tb.csw->data_residue = htole32(cur_cmd.data_residue); tb.csw->status = status; usb_drv_send_nonblocking(EP_IN, tb.csw, From 404e2c7626d1756d43316206f9263838f3453b58 Mon Sep 17 00:00:00 2001 From: Paul Sauro Date: Sun, 20 Sep 2026 15:21:56 +0200 Subject: [PATCH 193/408] usb: validate configuration descriptor indices and failed drivers Reject the first out-of-range configuration index and omit class descriptors for drivers whose initialization failed. Change-Id: I1f74dcceb69f19b52650aa6ae3f38331e320b151 --- firmware/usbstack/usb_core.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firmware/usbstack/usb_core.c b/firmware/usbstack/usb_core.c index e8a554db24..ca84122341 100644 --- a/firmware/usbstack/usb_core.c +++ b/firmware/usbstack/usb_core.c @@ -779,7 +779,7 @@ static void request_handler_device_get_descriptor(struct usb_ctrlrequest* req, u case USB_DT_OTHER_SPEED_CONFIG: case USB_DT_CONFIG: { - if(index > NUM_CONFIGS) { + if(index >= NUM_CONFIGS) { logf("invalid config dt index %u", index); break; } @@ -806,7 +806,7 @@ static void request_handler_device_get_descriptor(struct usb_ctrlrequest* req, u size = sizeof(struct usb_config_descriptor); for(i = 0; i < USB_NUM_DRIVERS; i++) { - if(drivers[i]->enabled && drivers[i]->config == index + 1 && drivers[i]->get_config_descriptor) { + if(drivers[i]->enabled && !drivers[i]->error && drivers[i]->config == index + 1 && drivers[i]->get_config_descriptor) { size += drivers[i]->get_config_descriptor(reqdata + size, max_packet_size); } } From 4dad9a0489eec6ba90404ef6554bbe352abcaaa7 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Sat, 19 Sep 2026 07:52:10 +1000 Subject: [PATCH 194/408] utils: iPod Nano 3G NAND and FTL tooling The host-side tools the Nano 3G NAND driver and FTL were developed and tested with, so the evidence in those changes can be reproduced and the next chip can be added without rediscovering any of it: regdiff (register- write comparison against Apple's sequencer programs), ftltest (the host FTL test suite), chiptable.py, the FTL decode notes, and RESULTS.md, the measurements the earlier changes quote. Co-authored-by: Claude Opus 5 Change-Id: I321e06291ca393e18dbd5f71c2c6371fcda9a94d --- utils/ipodnano3g/README | 94 +++ utils/ipodnano3g/RESULTS.md | 44 ++ utils/ipodnano3g/chiptable.py | 112 +++ utils/ipodnano3g/decode/APPLE-FTL-READ.md | 188 +++++ utils/ipodnano3g/decode/APPLE-FTL-WRITE.md | 84 +++ utils/ipodnano3g/ftltest/.gitignore | 14 + utils/ipodnano3g/ftltest/Makefile | 109 +++ utils/ipodnano3g/ftltest/bench_write.c | 126 ++++ utils/ipodnano3g/ftltest/ftl_hooks.c | 470 ++++++++++++ utils/ipodnano3g/ftltest/ftl_hooks.h | 26 + utils/ipodnano3g/ftltest/mock_nand.c | 793 ++++++++++++++++++++ utils/ipodnano3g/ftltest/mock_nand.h | 85 +++ utils/ipodnano3g/ftltest/replay.c | 140 ++++ utils/ipodnano3g/ftltest/stubs/config.h | 1 + utils/ipodnano3g/ftltest/stubs/kernel.h | 4 + utils/ipodnano3g/ftltest/stubs/logf.h | 2 + utils/ipodnano3g/ftltest/stubs/panic.h | 4 + utils/ipodnano3g/ftltest/stubs/storage.h | 1 + utils/ipodnano3g/ftltest/stubs/system.h | 4 + utils/ipodnano3g/ftltest/test_badblock.c | 154 ++++ utils/ipodnano3g/ftltest/test_crash.c | 232 ++++++ utils/ipodnano3g/ftltest/test_eligible.c | 118 +++ utils/ipodnano3g/ftltest/test_format.c | 239 ++++++ utils/ipodnano3g/ftltest/test_ftl.c | 191 +++++ utils/ipodnano3g/ftltest/test_geometry.c | 62 ++ utils/ipodnano3g/ftltest/test_mock.c | 141 ++++ utils/ipodnano3g/ftltest/test_permute.c | 805 +++++++++++++++++++++ utils/ipodnano3g/ftltest/test_restore.c | 149 ++++ utils/ipodnano3g/ftltest/test_wear.c | 157 ++++ utils/ipodnano3g/ftltest/test_write.c | 492 +++++++++++++ utils/ipodnano3g/regdiff/.gitignore | 3 + utils/ipodnano3g/regdiff/fmss_dis.py | 84 +++ utils/ipodnano3g/regdiff/fmss_emu.py | 161 +++++ utils/ipodnano3g/regdiff/regdiff.py | 215 ++++++ utils/ipodnano3g/regdiff/regdiff_read.py | 198 +++++ 35 files changed, 5702 insertions(+) create mode 100644 utils/ipodnano3g/README create mode 100644 utils/ipodnano3g/RESULTS.md create mode 100644 utils/ipodnano3g/chiptable.py create mode 100644 utils/ipodnano3g/decode/APPLE-FTL-READ.md create mode 100644 utils/ipodnano3g/decode/APPLE-FTL-WRITE.md create mode 100644 utils/ipodnano3g/ftltest/.gitignore create mode 100644 utils/ipodnano3g/ftltest/Makefile create mode 100644 utils/ipodnano3g/ftltest/bench_write.c create mode 100644 utils/ipodnano3g/ftltest/ftl_hooks.c create mode 100644 utils/ipodnano3g/ftltest/ftl_hooks.h create mode 100644 utils/ipodnano3g/ftltest/mock_nand.c create mode 100644 utils/ipodnano3g/ftltest/mock_nand.h create mode 100644 utils/ipodnano3g/ftltest/replay.c create mode 100644 utils/ipodnano3g/ftltest/stubs/config.h create mode 100644 utils/ipodnano3g/ftltest/stubs/kernel.h create mode 100644 utils/ipodnano3g/ftltest/stubs/logf.h create mode 100644 utils/ipodnano3g/ftltest/stubs/panic.h create mode 100644 utils/ipodnano3g/ftltest/stubs/storage.h create mode 100644 utils/ipodnano3g/ftltest/stubs/system.h create mode 100644 utils/ipodnano3g/ftltest/test_badblock.c create mode 100644 utils/ipodnano3g/ftltest/test_crash.c create mode 100644 utils/ipodnano3g/ftltest/test_eligible.c create mode 100644 utils/ipodnano3g/ftltest/test_format.c create mode 100644 utils/ipodnano3g/ftltest/test_ftl.c create mode 100644 utils/ipodnano3g/ftltest/test_geometry.c create mode 100644 utils/ipodnano3g/ftltest/test_mock.c create mode 100644 utils/ipodnano3g/ftltest/test_permute.c create mode 100644 utils/ipodnano3g/ftltest/test_restore.c create mode 100644 utils/ipodnano3g/ftltest/test_wear.c create mode 100644 utils/ipodnano3g/ftltest/test_write.c create mode 100644 utils/ipodnano3g/regdiff/.gitignore create mode 100644 utils/ipodnano3g/regdiff/fmss_dis.py create mode 100644 utils/ipodnano3g/regdiff/fmss_emu.py create mode 100644 utils/ipodnano3g/regdiff/regdiff.py create mode 100644 utils/ipodnano3g/regdiff/regdiff_read.py diff --git a/utils/ipodnano3g/README b/utils/ipodnano3g/README new file mode 100644 index 0000000000..08127a315d --- /dev/null +++ b/utils/ipodnano3g/README @@ -0,0 +1,94 @@ +# iPod Nano 3G storage validation + +Copyright (C) 2026 Andrew Rice. + +This directory contains the host-side evidence and reproducibility tools +used to validate the Nano 3G NAND and FTL implementation. The source files +here are distributed under the same GNU General Public License, version 2 +or later, as the Rockbox source tree. + +No Apple firmware, sequencer binary, NAND image, user data or block trace is +included. The tests that need those inputs require the person running them +to supply images obtained from their own device. + +## FTL tests + +`ftltest` builds `ftl-nano3g.c` against a copy-on-write NAND mock. It expects +two files in a directory named on each test's command line: + +- `nand_data_4banks.bin`: page data from a four-bank A514D3AD device, stored + bank by bank; +- `meta_all.bin`: 16 bytes per physical page, consisting of the three spare + words followed by the signed `nand_read_page()` result. + +The mock maps both inputs privately and cannot modify them. Run: + +``` +cd utils/ipodnano3g/ftltest +make check DUMP=/path/to/dump +make check-crash DUMP=/path/to/dump +``` + +`test_crash` provides deterministic power-cut and torn-write fault injection. +`replay` accepts a separately supplied block trace. + +The tests also take a medium spec in place of a dump directory: +`collected:DIR` is an archive from the NAND check below, and `blank:PRESET` +formats an empty medium in one of the chip table's layouts (see +`ftl_hooks.c`). + +## Validating a chip + +A chip's row in `nand_chip_table[]` +(`firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c`) says whether it +has been proven on hardware. Rockbox drives only validated chips: on any +other the bootloader says the NAND is not validated and starts Apple's +firmware. + +Validating one takes, in order: + +1. a check archive from the unit (below), which identifies the chip and + records its structures; +2. `test_ftl collected:DIR` and `test_crash collected:DIR 100 1 3000 0` + from `ftltest`, which mount that medium and exercise writes and power + cuts in its geometry on the host; +3. a write test on the unit itself, with a firmware built + `-DNAND_WRITABLE_ID=0x` (for example `-DNAND_WRITABLE_ID=0xA555D5AD`). + That mounts chips with this id writable without changing the table, so + an owner can copy a tree through Rockbox's USB mode, power off cleanly, + restart and check the files back. + +`-DNAND_TEST_READONLY` does the opposite: it treats every chip as +unvalidated, which is how the refusal path is tested on a validated unit. + +Only then is the row marked validated. + +## NAND check for contributors + +`nandcheck` builds a one-shot image, run from DFU, that identifies an iPod's +NAND chip, tries a read-only mount and serves the raw NAND read-only over +USB; `nandcheck.py collect` turns that into a small archive for validating +the chip with the tests above. Nothing is installed or written. See +`nandcheck/README.md`. + +## Sequencer register comparison + +`regdiff` compares the controller-register writes made by the NAND driver +with Apple's flash-controller sequencer programs. Set `NANO3G_OSOS` to a +decrypted firmware image obtained from the user's own device, then run: + +``` +NANO3G_OSOS=/path/to/osos.bin python3 utils/ipodnano3g/regdiff/regdiff.py +NANO3G_OSOS=/path/to/osos.bin python3 utils/ipodnano3g/regdiff/regdiff_read.py +``` + +The tools read the sequencer programs at their documented IRAM/file offsets; +they do not contain or create copies of those programs. Expected results are +recorded in `RESULTS.md`. + +## Decode notes + +`decode` records the independently decoded call structure, data formats and +sequencer behavior. Addresses identify locations in firmware 1.1.3 so a +reviewer with their own image can reproduce the analysis. Raw firmware and +bulk disassembly listings are deliberately excluded. diff --git a/utils/ipodnano3g/RESULTS.md b/utils/ipodnano3g/RESULTS.md new file mode 100644 index 0000000000..ba565bd8ed --- /dev/null +++ b/utils/ipodnano3g/RESULTS.md @@ -0,0 +1,44 @@ +# Nano 3G storage validation results + +Results below were recorded on 2026-09-13. Hardware results used a 4 GB Nano +3G with Hynix NAND ID `0xA514D3AD`. + +## Host tests + +- The independent newest-page oracle agreed with `ftl_read()` for all + 1,982,464 logical sectors in the device image. +- Read/write, wear-level, bad-block injection, allocation eligibility, + restore and mock-NAND suites completed without failures. +- Power was cut at 300 operation boundaries in each of 48 deterministic + campaigns. Half modeled a torn page program or half-completed erase. All + acknowledged writes survived each remount and no page was programmed twice + or out of order. +- The 600-file write replay retained 66,388 copy-merge reads and 34,816 + delete-merge reads. Batched reads reduced these to 528 and 280 controller + calls respectively, averaging 125.7 and 124.3 pages per call. +- Driver and emulator register traces were identical in all write scenarios: + one page, single-plane rows, repeated-bank pages, and one through three + two-plane rows. +- Driver and emulator register traces were identical for one read, four-bank + reads, repeated-bank reads and a mixed duplicate-bank split. + +## Hardware tests + +- A complete NAND dump produced 184 VFL context pages that all passed the + format's checksums. +- Rockbox mounted and restored the uncommitted state left by Apple disk mode. +- A 77,067,368-byte, 600-file tree written through Rockbox read back with all + SHA-256 hashes intact through both Rockbox and Apple disk mode. +- The same tree read at 7.550 MB/s through Rockbox USB and 9.745 MB/s through + Apple disk mode. Rockbox wrote and synced it at 0.731 MB/s; Apple wrote and + synced it at 1.026 MB/s. +- A setting survived reset and restore, and Apple firmware subsequently read + the volume successfully. + +Bad-block remapping and forced program/erase failure recovery have extensive +host fault-injection coverage, including validation of the persisted remap +after remount. They have not been deliberately triggered on hardware. The +implemented layout and recovery order follow the decoded Apple FTL/VFL +process documented in `decode/APPLE-FTL-WRITE.md`; that correspondence is +evidence for the implementation, not a substitute for destructive hardware +fault testing. diff --git a/utils/ipodnano3g/chiptable.py b/utils/ipodnano3g/chiptable.py new file mode 100644 index 0000000000..e8df0f60ce --- /dev/null +++ b/utils/ipodnano3g/chiptable.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Check nand-nano3g.c's chip table against Apple's and against the FTL. + +Every row of nand_chip_table[] must equal a row of the chip table in a +decrypted Nano 3G firmware 1.1.3 image (NANO3G_OSOS), in the same order, and +the geometry the driver and FTL derive from each row must fit the FTL's +static bounds. Rows the FTL does not mount yet are reported, not failed. +""" +import os +import re +import struct +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROCKBOX = os.path.abspath(os.path.join(HERE, "..", "..")) +TARGET = ROCKBOX + "/firmware/target/arm/s5l8702/ipodnano3g" +OSOS = os.environ.get("NANO3G_OSOS") + +# Firmware 1.1.3: the table sits at this file offset, 18 rows of 44 bytes +APPLE_TABLE, APPLE_ROWS, APPLE_ROW = 0x9269bc, 18, 44 + + +def ours(): + src = open(TARGET + "/nand-nano3g.c").read() + body = src[src.index("nand_chip_table[] ="):] + body = body[:body.index("};")] + rows = [] + for m in re.finditer(r"\{\s*(0x[0-9A-Fa-f]+),\s*(\d+),\s*(\d+),\s*(\d+)," + r"\s*(\d+),\s*(\d+),\s*(\d+),\s*(true|false)\s*\}", + body): + rows.append(dict(id=int(m.group(1), 16), banks=int(m.group(2)), + mode=int(m.group(3)), blocks=int(m.group(4)), + ppb=int(m.group(5)), pagesize=int(m.group(6)), + user=int(m.group(7)), validated=m.group(8) == "true")) + return rows + + +def apple(): + d = open(OSOS, "rb").read() + rows = [] + for n in range(APPLE_ROWS): + e = d[APPLE_TABLE + n * APPLE_ROW:][:APPLE_ROW] + cid, banks, blocks, ppb, spp = struct.unpack_from(" lim] + mib = sbpages * r["pagesize"] / (1 << 20) + maxpage = nand.get("NAND_MAX_PAGE_SIZE", nand["NAND_PAGE_SIZE"]) + pending = "; page size %d not supported yet" % r["pagesize"] \ + if r["pagesize"] > maxpage else "" + fails += bool(over) + print("%s row %2d mode %2d P %d: %4d sb, %4d logical, %4d pages/sb" + " (%g MiB), vflspares %3d%s%s" % + ("FAIL" if over else "PASS", n, r["mode"], p, nsb, usersb, + sbpages, mib, spares, + "".join("; %s %d > %d" % o for o in over), pending)) + print("ALL PASS" if not fails else "%d FAILED" % fails) + return 1 if fails else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/utils/ipodnano3g/decode/APPLE-FTL-READ.md b/utils/ipodnano3g/decode/APPLE-FTL-READ.md new file mode 100644 index 0000000000..f0b2e4a75a --- /dev/null +++ b/utils/ipodnano3g/decode/APPLE-FTL-READ.md @@ -0,0 +1,188 @@ +# Apple Nano 3G FTL read path (osos 1.1.3, Whimory 2.1), decoded 2026-09-13 + +Claims use **[measured]**, **[decoded]**, **[inferred]** and **[unknown]** as +defined by the project notes. Addresses are ARM addresses in +`n3g-dump/osos-1.1.3.bin`; the FMSS program is at IRAM address 0x22009030. + +| function | address | identification | +|---|---:|---| +| `FTL_Read` | 0x0803ace4 | **[decoded]** AND dispatcher call and arguments | +| `_FTLRead` | 0x080e3514 | **[decoded]** internal mapping/batched read | +| `VFL_Read` | 0x0805e8e8 | **[decoded]** single-page read and retry | +| `VFL_ReadMultiplePagesInVb` | 0x0805ecc8 | **[decoded]** sequential virtual-block read | +| `VFL_ReadScatteredPagesInVb` | 0x0805f0b8 | **[decoded]** VPN-list read | +| `m1fmssReadScatteredPages` | 0x082d3c78 | **[decoded]** FIL list builder | +| `m1fmssReadSequentialPages` | 0x082d3de0 | **[decoded]** FIL row-list builder | +| FMSS read program | 0x22009030 | **[decoded]** common program used by both FIL calls | + +## FTL_Read and _FTLRead + +`FTL_Read(lpn, count, buffer)` updates read statistics, calls +`_FTLRead(lpn, count, buffer)`, logs a nonzero result, services the refresh +list when it contains more than two entries (or a secondary counter passes +its threshold), and returns `_FTLRead`'s result. The AND call dispatcher at +0x080279bc loads the same three arguments before calling it. **[decoded]** + +`_FTLRead` rejects a null buffer, a zero/out-of-range request, and a buffer +whose address is not four-byte aligned. It splits the request at logical +block boundaries. For each logical block it looks up its log entry. **[decoded]** + +- With no log entry, it issues one `VFL_ReadMultiplePagesInVb(data_vbn, + page_offset, count, buffer, spares, &refresh)` for the whole portion of the + logical block. **[decoded]** +- With a log entry, it resolves every requested LPN to either the log block + page or the data block page, builds a VPN list, and issues one + `VFL_ReadScatteredPagesInVb(vpn_list, count, buffer, spares, &refresh)`. + **[decoded]** +- It then checks every returned 12-byte spare. A written page's stored LPN + must equal the requested LPN. An ECC mark other than 0xff is reported as an + error and the logical block is added to the refresh list. **[decoded]** +- If a multi-page VFL call fails, it retries each page separately through + `VFL_Read`, preserving the page's resolved virtual address. A failing page + or an unacceptable ECC mark makes the overall call fail, but the loop can + continue far enough to report more detail. **[decoded]** + +The merge/copy callers use the same fallback: `_DoSimpleMerge` and the block +copy at 0x080e4030 read `pages_per_superblock / 8` pages with `_FTLRead`; on +failure they retry each page with `_FTLRead(..., 1, ...)`, and put 0x55 in +byte 10 of that page's new 12-byte spare if the retry also fails. The 0x55 is +written with the destination block during the following write run; it is not +written back into the source page. **[decoded]** + +The Nano 2G implementation selects a bank-wide fast path only when aligned +and when no log is present. Nano 3G Apple firmware instead supplies the whole +logical-block portion to one of the two VFL multi-page interfaces, including +mixed data/log runs through the scattered interface. **[decoded]** + +## VFL splitting and error handling + +`VFL_ReadMultiplePagesInVb(vbn, start, count, buffer, spares, &refresh)` maps +the virtual block independently on every bank, then splits the run according +to the four-bank row width: **[decoded]** + +1. A head ending at the next row boundary is sent to the scattered FIL call, + unless it is a complete row. +2. While more than one complete row remains, the largest multiple of four + leaving the final row/tail is sent to the sequential FIL call. +3. The remaining one through four pages are sent to the scattered FIL call. + +Thus a row-aligned run of four pages is deliberately a scattered FIL call; +eight pages become four sequential plus four scattered. This is the same +head / whole rows while more than one row remains / tail policy used by +Apple's VFL write path. Reads have no two-plane eligibility test. **[decoded]** + +For each subcall the VFL advances data by 2048 bytes/page, spare data by 12 +bytes/page, and the virtual-page position by the number completed. It returns +1 only when every FIL subcall returns either 0 or 1; other FIL status values +produce VFL failure (0). The maximum corrected-bit count returned by the FIL +is compared with the device threshold byte at device-info +0x0f; reaching or +exceeding it sets `*refresh = 1`. **[decoded]** + +`VFL_ReadScatteredPagesInVb` maps every input VPN into parallel bank and +physical-page arrays, calls the scattered FIL once for the full list, applies +the same corrected-bit threshold, and normalizes FIL status 0 or 1 to VFL +success 1. **[decoded]** + +`VFL_Read` maps one VPN and calls the mode's single-page FIL read. On the +relevant failures it resets the NAND bank and retries once. Its later path can +schedule a block remap and update per-block read accounting. **[decoded]** + +The refresh list and its handler mutate FTL state and can ultimately cause +NAND writes. Rockbox currently has no equivalent. Porting that mechanism is +outside the read batching itself and requires the user's explicit scope +decision before implementation. **[decoded]** + +## FIL parameter block + +Both FIL functions run program 0x22009030 and write the same sequencer +parameter block. **[decoded]** + +| offset | meaning | +|---:|---| +| `SEQ+0xc04` | program address, 0x22009030 | +| `SEQ+0xd04` | bank count (4 on Nano 3G) | +| `SEQ+0xd0c` | pointer to u32 physical-page list | +| `SEQ+0xd10` | pointer to u32 logical-bank list | +| `SEQ+0xd14` | pointer to bank-to-chip-enable table | +| `SEQ+0xd18` | number of pages | +| `SEQ+0xd1c` | pointer to packed 12-byte spare outputs | +| `SEQ+0xd20` | pointer to the page/chunk DMA-address list | +| `SEQ+0xd24` | pointer to one u32 ECC/result word per page | +| `SEQ+0xd28` | 2KiB units per page (sectors-per-page / 4 = 1 here) | +| `SEQ+0xd48` | base `FMCTRL0` bits from the mode descriptor | +| `SEQ+0xd4c` | additional `FMCTRL0` bits from the mode descriptor | + +The scattered FIL accepts parallel bank/page inputs and expands a contiguous +data buffer into four 512-byte DMA addresses per page. The sequential FIL +accepts one starting physical page per bank and expands it row by row into +the same page and bank lists; its buffer layout is likewise expanded into +four chunk addresses per page. Both call the same completion routine and +then classify the aggregate result at `SEQ+0xc30`. **[decoded]** + +The result classifier treats bit 30 as a hard FIL failure. Bit 29 identifies +the erased/empty-page condition used by a single-page caller. The low byte is +a bitmap of corrected-bit counts seen among the page's four chunks; the +highest set bit becomes the returned maximum corrected-bit count. **[decoded]** + +## FMSS opcodes 0x11, 0x18 and 0x19 + +All occurrences in 0x22009030 pin down the previously unknown operations: +**[decoded]** + +``` +0x11: mem32[rB] = rA +0x18: rA = seq32[rB] +0x19: seq32[rB] = rA +``` + +Opcode 0x11 stores the three spare words and the per-page result into ARM +memory. Opcodes 0x18/0x19 indirectly access sequencer-local registers/RAM. +The program uses 0x19 to build the unique chip-enable list at `SEQ+0xd60`, +then 0x18 to walk it. A later 0x18 loads the next chip enable through the +pointer at `SEQ+0xd50`. The disassembler and emulator now implement all +three operations. **[decoded]** + +## Program 0x22009030 + +The program first scans the supplied bank list, converts each bank through +the chip-enable table, and records each distinct chip enable in sequencer +memory at `SEQ+0xd60`. It then sends NAND READ (0x00), the row address and +READ START (0x30) for the first pending page on every distinct chip enable +before waiting for any one chip's ready status. This overlaps NAND page-load +time across banks. **[decoded]** + +For each page it selects the corresponding chip enable, waits for status +ready with command 0x70, returns to data output with command 0x00, and runs +four 512-byte data/ECC pipelines. Each chunk stages controller data, starts +the ECC transfer, records an uncorrectable indication as result bit 30 or a +corrected-bit-count indication in the result's low-byte bitmap, and DMA-writes +the corrected 512 bytes to its supplied address. **[decoded]** + +After the chunks it decodes and stores `FMSYND5..7` as the 12-byte spare, +stores the page result word, and ORs that word into the aggregate result. +When another page remains on that chip enable, it starts that NAND page load +before proceeding, maintaining one outstanding load per bank while pages on +other banks are transferred. Finally it clears the DMA/chip-select bits and +ends; the sequencer exposes the aggregate result at `SEQ+0xc30`. **[decoded]** + +The emulator now completes one-page and multi-bank runs. Its clean model +produces result word zero; setting `FMUNK810`'s corrected count field to 3 +produces low-byte result bit 2 (`0x4`) for each page, matching the classifier's +reported correction count of 3. These are emulator observations, not device +measurements. **[measured]** + +## Porting consequences + +- One Rockbox `nand_read_pages` call should accept an ordered physical-page, + bank, data-buffer and spare/result list and reproduce 0x22009030. A + single-page wrapper may use it only after register equivalence and hardware + validation. **[inferred]** +- FTL host reads should split only at logical-block boundaries. A block + portion without a log uses the multiple-VB path; one with a log builds a + scattered physical list. **[decoded]** +- Merge reads should retain Apple's 128-page grouping and page-by-page retry, + including the destination spare's 0x55 ECC mark after a repeated failure. + Any on-flash behavior will be called out before device testing. **[decoded]** +- The existing BootROM-derived `nand_read_page` remains the hardware-proven + fallback until the sequencer implementation has passed on the device. + **[inferred]** diff --git a/utils/ipodnano3g/decode/APPLE-FTL-WRITE.md b/utils/ipodnano3g/decode/APPLE-FTL-WRITE.md new file mode 100644 index 0000000000..7e9da9e3de --- /dev/null +++ b/utils/ipodnano3g/decode/APPLE-FTL-WRITE.md @@ -0,0 +1,84 @@ +# Apple Nano 3G FTL write path (osos 1.1.3, Whimory 2.1), decoded 2026-09-13 + +Disassembly in this directory (`*.s`, from `dis/fn.py`). Whimory source path +string: `Whimory2_1\core\FTL\FTLInter...`. Function names come from the +firmware's own log strings. + +| function | address | Nano 2G equivalent (ftl-nano2g.c) | +|---|---|---| +| `FTL_Write` | 0x0803af64 | `ftl_write()` | +| `_PrepareLog` | 0x0806b5ec | `ftl_allocate_log_entry()` | +| `_FindLogForLbn` | 0x08084998 | `ftl_get_log_entry()` | +| `_Merge` | 0x080dd8c4 | `ftl_remove_scattered_block()` | +| `_DoMoveMerge` | 0x0806f788 | `ftl_compact_scattered()` | +| `_DoCopyMerge` | 0x0806f488 | `ftl_commit_sequential()` | +| `_DoSimpleMerge` | 0x0807c1b0 | `ftl_commit_scattered()` | +| `_FTLMarkCxtNotValid` | 0x080a1e50 | clean_flag block in `ftl_write()` | +| `_GetFreeVb` / `_SetFreeVb` | 0x08067098 / 0x0806724c | `ftl_allocate_pool_block()` / `ftl_release_pool_block()` | +| `_AutoWearLevel` | 0x0807bdfc | `ftl_swap_blocks()` | +| `FTL_Flush` | 0x0803a404 | `ftl_sync()` | +| `_StoreFTLCxt` | 0x0806fc1c | `ftl_commit_cxt()` | +| `_FTLRestore` | 0x08069a4c | (none - nano2g refuses an unclean mount) | + +Globals: 0x08b5ed00 device info (+0x20 u16 pages per superblock, +0x2e u16 +user logical blocks, +0x30 u32 capacity in pages, +0x34 u16 page size); +0x08a34b7c pointers (+4 FTL context, +12 spare buffer array). +FTL context: +4 user usn, +8 freecount, +12 swapcounter, +0x198 map table +pointer, +0x1a4 17 log structs of 20 bytes, +0x318 ctrlpage, +0x31c clean +flag, +0x3c8 refresh-list count. +Log struct: +0 usn (recency), +4 vbn (0xffff none), +6 lbn, +8 offsets +table pointer, +12 pages used, +14 pages current, +16 is-sequential. + +## FTL_Write(lpn, count, buf) +- `_FTLMarkCxtNotValid()` at every call (writes the 0x4f mark only if clean). +- Per logical block: `log = _PrepareLog(lbn)`. + - Log full (pages used == ppb): `_Merge(log)`; if that freed the log's + block, `_GetFreeVb` a new one. + - Whole-block write (count >= ppb, offset 0): into the log block if it is + empty, else into a fresh `_GetFreeVb` block; then `_SetFreeVb` the log + block and the old data block, map[lbn] = new block, log vbn = 0xffff. + - Else append min(ppb - offset, count, ppb - used) pages to the log with + `VFL_WriteMultiplePagesInVb` (0x0805f8f0), updating offsets; sequential + stays set only while every page lands at its own index. + - Last page of a block gets type 0x41 when whole-block or still sequential. + - A full, sequential log is merged immediately (becomes the data block). +- **usn (context +4) increments only when a write starts at page 0 of a + block**; all pages appended to a log share that usn. (Matches the dump: + Apple's logs of 8 and 4 pages each carry one usn.) +- Program failure: log marked full and non-sequential (forces a merge), or a + fresh whole-block target is `_SetFreeVb`'d; then retry. No panic. +- **No commit.** Afterwards: refresh list > 2 entries -> handle refresh; + else swapcounter >= 300 -> swapcounter -= 20 and up to 4 `_AutoWearLevel` + (identical to nano2g). + +## _PrepareLog(lbn) (= nano2g ftl_allocate_log_entry) +- Existing log for lbn: log usn = context usn - 1 (recency), return it. +- Else reuse a slot that has a block but no pages. +- Else if freecount == 3: `_Merge(NULL)`. +- Then take a free slot, `_GetFreeVb` its block. + +## _Merge(log) (= nano2g ftl_remove_scattered_block) +- `_FTLMarkCxtNotValid()`. +- log NULL: victim = lowest usn, ties -> more current pages; sequential -> + `_DoCopyMerge`, else `_DoSimpleMerge`. +- log given: current pages <= ppb/2 -> **`_DoMoveMerge` (compaction)**; + else sequential -> `_DoCopyMerge`, else `_DoSimpleMerge`. +- swapcounter++ per merge. + +## FTL_Flush(mode) +- Clean flag set: return. +- **mode 1: fold every log** (sequential -> `_DoCopyMerge`, else + `_DoSimpleMerge`, swapcounter++ each), then store. +- **other modes: store without folding - logs stay open, recorded in the + context and the 17 0x45 offset-table pages.** The measured dump's commit + (18 free, 2 logs, non-FF 0x45 data) is one of these. +- `_StoreFTLCxt` up to 5 tries, moving ctrlpage to a block end on failure. +- Callers: only the AND call dispatcher (0x080279e4, mode from the OS) and + the end of `_FTLRestore`. Never from the write path. + +## _FTLRestore +Closed blocks (last page 0x41) -> map, highest usn wins; unclosed with page 0 +data -> logs; one log per lbn (loser of an offset-table comparison dropped); +more than 17 logs fails (then FTL_Open fails, "erasing block 0" = reformat); +holes filled with erased blocks; free pool = the rest, must total 20 with +logs; whole pool erased; then FTL_Flush. diff --git a/utils/ipodnano3g/ftltest/.gitignore b/utils/ipodnano3g/ftltest/.gitignore new file mode 100644 index 0000000000..53e65b2e12 --- /dev/null +++ b/utils/ipodnano3g/ftltest/.gitignore @@ -0,0 +1,14 @@ +bench_write +ftl_built.c +replay +test_badblock +test_crash +test_eligible +test_ftl +test_mock +test_restore +test_wear +test_write +test_geometry +test_format +test_permute diff --git a/utils/ipodnano3g/ftltest/Makefile b/utils/ipodnano3g/ftltest/Makefile new file mode 100644 index 0000000000..4b655784ef --- /dev/null +++ b/utils/ipodnano3g/ftltest/Makefile @@ -0,0 +1,109 @@ +R ?= ../../.. +T = $(R)/firmware/target/arm/s5l8702/ipodnano3g +CFLAGS = -O2 -g -std=gnu99 -Wall -Wextra -Wno-unused-parameter -Istubs -I$(T) \ + -include ftl_hooks.h +DUMP ?= + +# The test hooks are appended to the FTL source, each group only if that +# tree does not already define it. +HOOKFLAGS = $(shell grep -q 'int ftl_sb_locate' $(FTL) || echo -DNEED_HOOKS_LOCATE) \ + $(shell grep -q 'uint32_t ftl_pool_snapshot' $(FTL) || echo -DNEED_HOOKS_WEAR) + +FTL = $(T)/ftl-nano3g.c +HDRS = $(T)/ftl-target.h $(T)/nand-target.h mock_nand.h ftl_hooks.h ftl_hooks.c + +ftl_built.c: $(FTL) ftl_hooks.c FORCE + cat $(FTL) ftl_hooks.c > $@ +FORCE: + +all: test_ftl test_mock test_write test_wear test_badblock test_eligible test_restore \ + test_geometry test_format test_permute bench_write + +test_ftl: ftl_built.c mock_nand.c test_ftl.c $(HDRS) + $(CC) $(CFLAGS) $(HOOKFLAGS) -o $@ ftl_built.c mock_nand.c test_ftl.c + +test_mock: mock_nand.c test_mock.c $(HDRS) + $(CC) $(CFLAGS) -o $@ mock_nand.c test_mock.c + +test_write: ftl_built.c mock_nand.c test_write.c $(HDRS) + $(CC) $(CFLAGS) $(HOOKFLAGS) -o $@ ftl_built.c mock_nand.c test_write.c + +test_wear: ftl_built.c mock_nand.c test_wear.c $(HDRS) + $(CC) $(CFLAGS) $(HOOKFLAGS) -o $@ ftl_built.c mock_nand.c test_wear.c + +test_badblock: ftl_built.c mock_nand.c test_badblock.c $(HDRS) + $(CC) $(CFLAGS) $(HOOKFLAGS) -o $@ ftl_built.c mock_nand.c test_badblock.c + +test_eligible: ftl_built.c mock_nand.c test_eligible.c $(HDRS) + $(CC) $(CFLAGS) $(HOOKFLAGS) -o $@ ftl_built.c mock_nand.c test_eligible.c + +test_restore: ftl_built.c mock_nand.c test_restore.c $(HDRS) + $(CC) $(CFLAGS) $(HOOKFLAGS) -o $@ ftl_built.c mock_nand.c test_restore.c + +test_permute: mock_nand.c test_permute.c $(HDRS) + $(CC) $(CFLAGS) -o $@ mock_nand.c test_permute.c + +test_geometry: ftl_built.c mock_nand.c test_geometry.c $(HDRS) + $(CC) $(CFLAGS) $(HOOKFLAGS) -o $@ ftl_built.c mock_nand.c test_geometry.c + +test_format: ftl_built.c mock_nand.c test_format.c $(HDRS) + $(CC) $(CFLAGS) $(HOOKFLAGS) -o $@ ftl_built.c mock_nand.c test_format.c + +replay: ftl_built.c mock_nand.c replay.c $(HDRS) + $(CC) $(CFLAGS) $(HOOKFLAGS) -o $@ ftl_built.c mock_nand.c replay.c + +test_crash: ftl_built.c mock_nand.c test_crash.c $(HDRS) + $(CC) $(CFLAGS) $(HOOKFLAGS) -o $@ ftl_built.c mock_nand.c test_crash.c + +bench_write: ftl_built.c mock_nand.c bench_write.c $(HDRS) + $(CC) $(CFLAGS) $(HOOKFLAGS) -o $@ ftl_built.c mock_nand.c bench_write.c + +check: all + test -n "$(DUMP)" + ./test_mock $(DUMP) + ./test_ftl $(DUMP) + ./test_permute $(DUMP) + ./test_ftl halves:$(DUMP) + ./test_ftl both:$(DUMP) + ./test_write $(DUMP) + ./test_wear $(DUMP) + ./test_badblock $(DUMP) + ./test_eligible $(DUMP) + ./test_restore $(DUMP) + ./test_geometry $(DUMP) + ./test_format blank:2k4 + ./test_format blank:2k8g + ./test_format blank:4k2 + ./test_format blank:halves + ./test_format blank:both + ./test_format blank:single4k + ./test_format blank:split13 + +# Power-cut campaigns on formatted blank media, 4KiB pages included +check-crash-blank: test_crash + for g in 2k4 4k2 halves both single4k split13; do for s in 1 2; do \ + ./test_crash blank:$$g 300 $$s 6000 0 || exit 1; \ + ./test_crash blank:$$g 300 $$s 6000 1 || exit 1; \ + done; done + +# Power-cut campaigns on the dump laid out as modes 9 and 4 +check-crash-layouts: test_crash + test -n "$(DUMP)" + for g in halves both; do for s in 1 2 3 4; do \ + ./test_crash $$g:$(DUMP) 300 $$s 6000 0 || exit 1; \ + ./test_crash $$g:$(DUMP) 300 $$s 6000 1 || exit 1; \ + done; done + +check-crash: test_crash + test -n "$(DUMP)" + for s in $$(seq 1 24); do \ + ./test_crash $(DUMP) 300 $$s 6000 0; \ + ./test_crash $(DUMP) 300 $$s 6000 1; \ + done + +clean: + rm -f ftl_built.c test_ftl test_mock test_write test_wear test_badblock \ + test_eligible test_restore test_geometry test_format test_permute \ + test_crash replay bench_write + +.PHONY: all check check-crash check-crash-blank check-crash-layouts clean FORCE diff --git a/utils/ipodnano3g/ftltest/bench_write.c b/utils/ipodnano3g/ftltest/bench_write.c new file mode 100644 index 0000000000..f444904cce --- /dev/null +++ b/utils/ipodnano3g/ftltest/bench_write.c @@ -0,0 +1,126 @@ +/* What one flush costs, in page programs and block erases, for a few write + * shapes. Amplification is measured against the bytes the caller asked to + * write. */ +#include +#include +#include +#include "nand-target.h" +#include "ftl-target.h" +#include "mock_nand.h" + +#define SBPAGES 1024 + +static uint8_t pat[NAND_PAGE_SIZE]; +static uint32_t *lpns; +static uint32_t nlpn; + +static const void *src(uint32_t lpn, void *ctx) +{ + uint32_t i; + + (void)ctx; + for (i = 0; i < nlpn; i++) + if (lpns[i] == lpn) + return pat; + return NULL; +} + +#define ROUNDS 10 + +static void bench(const char *what, uint32_t n, uint32_t stride, uint32_t base) +{ + uint32_t i, r; + double amp; + + nlpn = n; + for (i = 0; i < n; i++) + lpns[i] = base + i * stride; + mock_reset_stats(); + for (r = 0; r < ROUNDS; r++) + if (ftl_hook_write_merge(lpns[0], lpns[n - 1], src, NULL) || ftl_sync()) + { + printf(" %-38s FAILED\n", what); + return; + } + amp = (double)mock_writes / ROUNDS / n; + printf(" %-38s %7.0f pages, %5.1f block erases, %6.0fx\n", what, + (double)mock_writes / ROUNDS, (double)mock_erases / ROUNDS, amp); +} + +int main(int argc, char **argv) +{ + uint32_t i; + + mock_open(argc > 1 ? argv[1] : "../n3g-dump"); + if (ftl_init()) { puts("mount failed"); return 1; } + lpns = malloc(4096 * sizeof(uint32_t)); + for (i = 0; i < NAND_PAGE_SIZE; i++) + pat[i] = (uint8_t)i; + /* Settle first. A freshly mounted unit has an erase-count spread of + * about 110, so static wear levelling relocates one cold block per + * commit until the spread is under the threshold; that is a real cost + * but a transient one, and it would otherwise be charged to every row + * below. Run until it stops firing. */ + { + uint16_t psb[20], pec[20]; + uint32_t r, n, i, mn, mx; + + ftl_sync(); + ftl_init(); + for (r = 0; r < 200; r++) + { + n = ftl_pool_snapshot(psb, pec, 20); + mn = 0xffff; mx = 0; + for (i = 0; i < n; i++) + { + if (pec[i] < mn) mn = pec[i]; + if (pec[i] > mx) mx = pec[i]; + } + if (n && mx - mn <= 64) + break; + lpns[0] = 900 * SBPAGES + 1; + nlpn = 1; + if (ftl_hook_write_merge(lpns[0], lpns[0], src, NULL) || ftl_sync()) + break; + } + printf("settled after %u levelling rounds " + "(pool erase counts %u..%u)\n\n", r, mn, mx); + ftl_init(); + } + + printf("cost of one flush, averaged over %d, including the 25-page\n" + "commit and any wear-levelling relocation it triggers\n", ROUNDS); + printf(" %-38s %7s %5s %6s\n", "", "written", "erased", "amp"); + bench("1 sector", 1, 1, 100 * SBPAGES + 7); + bench("8 sectors, contiguous", 8, 1, 110 * SBPAGES + 7); + bench("512 sectors, contiguous (1 MiB)", 512, 1, 120 * SBPAGES); + bench("1024 sectors = one whole block (2 MiB)", 1024, 1, 130 * SBPAGES); + bench("2 sectors, 2 blocks apart", 2, SBPAGES, 140 * SBPAGES + 3); + bench("8 sectors, 1 per block", 8, SBPAGES, 150 * SBPAGES + 3); + /* What the storage layer actually does: absorb writes in the overlay, + * push them into log blocks when it fills, and commit only at shutdown. */ + { + uint32_t r, blocks[8] = { 60, 61, 75, 90, 91, 120, 121, 140 }; + + mock_reset_stats(); + for (r = 0; r < 40; r++) + { + nlpn = 1; + lpns[0] = blocks[r % 8] * SBPAGES + 3 + r; + if (ftl_hook_write_merge(lpns[0], lpns[0], src, NULL)) + break; + } + printf("\n 40 scattered 1-sector writes, no sync yet:\n" + " %lu pages written, %lu block erases\n", + mock_writes, mock_erases); + ftl_sync(); + printf(" then one sync (folds the logs):\n" + " %lu pages written, %lu block erases in total, %.0fx\n", + mock_writes, mock_erases, (double)mock_writes / 40); + } + + printf("\none superblock = %d pages = %d KiB; erases are physical blocks,\n", + SBPAGES, SBPAGES * NAND_PAGE_SIZE / 1024); + printf("8 per superblock (4 banks x 2 planes)\n"); + return 0; +} diff --git a/utils/ipodnano3g/ftltest/ftl_hooks.c b/utils/ipodnano3g/ftltest/ftl_hooks.c new file mode 100644 index 0000000000..44ead7a21e --- /dev/null +++ b/utils/ipodnano3g/ftltest/ftl_hooks.c @@ -0,0 +1,470 @@ +/* Query hooks for the host tests. They read the FTL's static state, so this + * file is appended to ftl-nano3g.c at build time (see the Makefile) rather + * than compiled on its own. A hook group is only appended when the FTL + * source lacks it, so this also + * builds against trees that still define them. */ + +#include "mock_nand.h" + +#ifdef NEED_HOOKS_LOCATE +/* The remap slot whose reserved block is physical block block: what it + * replaces, or 0xfff0/0xffff; -1 if block is not a reserved block */ +int ftl_vfl_pool_entry(uint32_t bank, uint32_t block) +{ + uint32_t k; + + if (!geo || bank >= geo->banks) + return -1; + for (k = 0; k < geo->planes * geo->vflspares; k++) + if (ftl_vfl_spare_block(k) == block) + return ftl_vfl_cxt[bank].remap[k]; + return -1; +} + +int ftl_sb_locate(uint32_t sb, uint32_t v, uint32_t *bank, uint32_t *page) +{ + if (!ftl_mounted || sb >= nsuperblocks || v >= sbpages) + return -1; + ftl_vpage_phys(sb, v, bank, page); + return 0; +} + +int ftl_locate(uint32_t lpn, uint32_t *bank, uint32_t *page) +{ + uint32_t sb, v; + + if (!ftl_mounted || ftl_resolve(lpn, &sb, &v)) + return -1; + ftl_vpage_phys(sb, v, bank, page); + return 0; +} +#endif + +#if defined(NEED_HOOKS_WEAR) && defined(FTL_COMMIT_PAGES) /* needs the write path */ +void ftl_global_spread(uint32_t *lowest, uint32_t *highest) +{ + uint32_t i, mn = 0xffff, mx = 0; + + for (i = 0; i < nsuperblocks; i++) + { + if (ftl_erasectr[i] == 0xffff) + continue; + if (ftl_erasectr[i] < mn) mn = ftl_erasectr[i]; + if (ftl_erasectr[i] > mx) mx = ftl_erasectr[i]; + } + *lowest = mn; + *highest = mx; +} + +uint32_t ftl_pool_snapshot(uint16_t *sb, uint16_t *erasectr, uint32_t max) +{ + uint32_t i, n = 0; + + if (!ftl_mounted) + return 0; + for (i = 0; i < ftl_cxt.freecount && n < max; i++) + { + sb[n] = ftl_cxt.blockpool[(ftl_cxt.nextfreeidx + i) % FTL_POOL_SIZE]; + erasectr[n] = sb[n] < nsuperblocks ? ftl_erasectr[sb[n]] : 0xffff; + n++; + } + return n; +} +#endif + +/* Pool state behind ftl_pool_alloc()'s choice: blocks free at the last + * commit are safe to hand out, blocks released since are not */ +void ftl_pool_state(uint32_t *freecount, uint32_t *fresh, uint32_t *nlogs) +{ + *freecount = ftl_cxt.freecount; + *fresh = 0; /* no fresh-block rule any more */ + *nlogs = ftl_nlogs; +} + +/* ftl_write_merge() and the write state are private to the FTL; the tests + * reach them through these */ +int ftl_hook_write_merge(uint32_t first, uint32_t last, + const void *(*src)(uint32_t lpn, void *ctx), + void *ctx) +{ + return ftl_write_merge(first, last, src, ctx); +} + +bool ftl_hook_writeable(void) +{ + return ftl_mounted && ftl_writable; +} + +/* The three FTL control superblocks, so a test can roll them back */ +void ftl_hook_ctrl(uint16_t *sb) +{ + unsigned int i; + + for (i = 0; i < 3; i++) + sb[i] = ftl_ctrl[i]; +} + +/* Consistency of the mounted FTL's RAM state against itself and the medium. + * Returns 0, or writes a description of the first problem into msg. */ +int ftl_hook_check(char *msg, size_t len) +{ + static uint8_t use[FTL_MAX_SB]; /* 1 map, 2 pool, 3 log, 4 ctrl */ + uint32_t i, v, sb; + + memset(use, 0, sizeof(use)); + for (i = 0; i < 3; i++) + if (ftl_ctrl[i] < nsuperblocks) + use[ftl_ctrl[i]] = 4; + for (i = 0; i < usersb; i++) + { + sb = ftl_map[i]; + if (use[sb]) + { + snprintf(msg, len, "map lblock %u -> sb %u, already used as %u", i, sb, use[sb]); + return -1; + } + use[sb] = 1; + } + for (i = 0; i < ftl_nlogs; i++) + { + const struct ftl_log *log = &ftl_logs[i]; + sb = log->sb; + if (use[sb]) + { + snprintf(msg, len, "log %u (lblock %u) sb %u already used as %u", i, log->lblock, sb, use[sb]); + return -1; + } + use[sb] = 3; + for (v = 0; v < sbpages; v++) + if (log->offsets[v] != FTL_NO_PAGE) + { + if (ftl_read_vpage(sb, log->offsets[v]) < 0 || !meta_is_data(ftl_meta) + || ftl_meta[0] != log->lblock * sbpages + v) + { + snprintf(msg, len, "log lblock %u idx %u -> sb %u v %u holds lpn %x type %x", + log->lblock, v, sb, log->offsets[v], ftl_meta[0], meta_type(ftl_meta)); + return -1; + } + } + } + if (1) + for (i = 0; i < ftl_cxt.freecount; i++) + { + sb = ftl_cxt.blockpool[(ftl_cxt.nextfreeidx + i) % FTL_POOL_SIZE]; + if (use[sb]) + { + snprintf(msg, len, "pool sb %u already used as %u", sb, use[sb]); + return -1; + } + use[sb] = 2; + } + if (ftl_cxt.freecount + ftl_nlogs > FTL_POOL_SIZE) + { + snprintf(msg, len, "free %u + logs %u > 20", ftl_cxt.freecount, ftl_nlogs); + return -1; + } + if (!ftl_writable) + { + snprintf(msg, len, "not writable after mount"); + return -1; + } + return 0; +} + +/* What ftl_load_cxt() sees in the three control blocks */ +void ftl_hook_ctrl_summary(void) +{ + uint32_t i, v, last; + int type, lt; + + for (i = 0; i < 3; i++) + { + uint32_t u0 = 0; + int t0 = ftl_read_vpage(ftl_ctrl[i], 0); + u0 = ftl_meta[0]; + last = 0; lt = -9; + for (v = 0; v < sbpages; v++) + { + type = ftl_read_vpage(ftl_ctrl[i], v); + if (type == SPARE_ERASED) break; + lt = type; last = v; + } + printf(" ctrl %u = sb %u: page0 type %x usn %08x, used %u, last type %x usn %08x\n", + i, ftl_ctrl[i], t0, u0, v, lt, ftl_meta[0]); + (void)last; + } + printf(" ftl_ctrl_idx %u ftl_ctrl_v %u cxt.usn %08x cleanflag %u maxusn %u\n", + ftl_ctrl_idx, ftl_ctrl_v, ftl_cxt.usn, ftl_cxt.cleanflag, ftl_cxt.maxusn); +} + +/* Fold every open log and commit, leaving the medium's data unchanged: + * sector 0 written back as it is, then a full flush and a remount */ +int ftl_hook_settle(void) +{ + static uint8_t s0[NAND_PAGE_SIZE]; + + if (ftl_read(0, 1, s0) || ftl_write(0, 1, s0) || ftl_sync()) + return -1; + return ftl_init(); +} + +/* Lay down a clean, empty volume on a blank mock medium in Apple's format, + * for geometries no dump exists of: per bank a VFL context whose ring is + * blocks 1-4, with the blocks they and block 0 fall in remapped to the + * first reserved blocks as on the dumped unit; FTL control superblocks 0-2; + * logical block n in superblock 3 + n; the 20 after them free; then one + * commit. Every data page reads erased. mock_open_blank() first. */ +int ftl_hook_format(void) +{ + uint32_t bank, i, n; + + ftl_mounted = false; + mutex_init(&ftl_mtx); + ftl_nholes = 0; + if (ftl_setup_geometry()) + return -1; + ftl_vfl_usn = 0; + for (bank = 0; bank < geo->banks; bank++) + { + struct ftl_vfl_cxt *cxt = &ftl_vfl_cxt[bank]; + uint32_t used[4] = { 0, 0, 0, 0 }; + + memset(cxt, 0, sizeof(*cxt)); + cxt->updatecount = 0xffffffff; + for (i = 0; i < 3; i++) + cxt->ftlctrlblocks[i] = i; + for (i = 0; i < 4; i++) + cxt->vflcxtblocks[i] = 1 + i; + for (i = 0; i < geo->planes * geo->vflspares; i++) + cxt->remap[i] = FTL_VFL_FREE; + /* Physical blocks 0-4, each in the table of the unit it belongs to */ + for (i = 0; i <= 4; i++) + { + uint32_t unit = 0, v = 0; + + while (ftl_unit_block(unit, v) != i) + if (++v == 8) + { + v = 0; + unit++; + } + cxt->remap[unit * geo->vflspares + used[unit]++] = i; + } + for (i = 0; i < 4; i++) + cxt->usedcount[i] = used[i]; + if (ftl_vfl_store_cxt(bank)) + return -2; + } + + memcpy(ftl_ctrl, ftl_vfl_cxt[0].ftlctrlblocks, sizeof(ftl_ctrl)); + for (i = 0; i < usersb; i++) + ftl_map[i] = 3 + i; + memset(&ftl_cxt, 0, sizeof(ftl_cxt)); + memset(ftl_cxt.field_3d8, 0xff, sizeof(ftl_cxt.field_3d8)); + for (n = 0; n < FTL_POOL_SIZE; n++) + ftl_cxt.blockpool[n] = 3 + usersb + n; + ftl_cxt.freecount = FTL_POOL_SIZE; + ftl_cxt.usn = 0xfffffff0; + ftl_nlogs = 0; + memset(ftl_erasectr, 0, sizeof(ftl_erasectr)); + memset(ftl_readcount, 0, sizeof(ftl_readcount)); + memset(ftl_stats, 0xff, sizeof(ftl_stats)); + ftl_nmappages = (usersb * 2 + pagesize - 1) / pagesize; + ftl_necpages = (nsuperblocks * 2 + pagesize - 1) / pagesize; + ftl_nrcpages = ftl_necpages; + ftl_nlogpages = (sbpages * FTL_MAX_LOGS * 2 + pagesize - 1) / pagesize; + ftl_ctrl_idx = 0; + ftl_ctrl_v = 0; + ftl_mounted = true; + ftl_writable = true; + return ftl_commit() ? -3 : 0; +} + +/* A medium from what nandcheck.py collected from a unit: report.txt for the + * geometry, meta.bin for every page's spare record and pages.bin for every + * page that is not user data. User data was not collected and reads as + * zeros, which is enough to mount, resolve every logical page and write. */ +static int open_collected(const char *dir) +{ + char path[512], line[128]; + unsigned banks = 0, blocks = 0, ppb = 0, pagesize = 0, userblocks = 0; + unsigned mode = 0, planes = 0, vflspares = 0; + size_t npages, g; + uint32_t rec[2]; + FILE *f; + + snprintf(path, sizeof(path), "%s/report.txt", dir); + if (!(f = fopen(path, "r"))) + return -1; + while (fgets(line, sizeof(line), f)) + { + sscanf(line, "banks %u", &banks); + sscanf(line, "blocks %u", &blocks); + sscanf(line, "ppb %u", &ppb); + sscanf(line, "pagesize %u", &pagesize); + sscanf(line, "userblocks %u", &userblocks); + sscanf(line, "mode %u", &mode); + sscanf(line, "planes %u", &planes); + sscanf(line, "vflspares %u", &vflspares); + } + fclose(f); + if (!banks || !blocks || !ppb || !pagesize || !planes) + return -1; + mock_geo.banks = banks; + mock_geo.blocks = blocks; + mock_geo.pagesperblock = ppb; + mock_geo.pagesize = pagesize; + mock_geo.userblocks = userblocks; + mock_geo.planes = planes; + mock_geo.vflspares = vflspares; + mock_geo.mode = mode; + mock_geo.twoplane = mode == 8; + mock_geo.layout = mode == 1 ? NAND_LAYOUT_SINGLE + : mode == 3 || mode == 8 ? NAND_LAYOUT_ADJACENT + : mode == 2 || mode == 9 || mode == 12 ? NAND_LAYOUT_HALVES + : mode == 4 ? NAND_LAYOUT_BOTH + : mode == 13 ? NAND_LAYOUT_SPLIT13 : NAND_LAYOUT_UNKNOWN; + mock_geo.validated = true; /* a host copy: writes are safe */ + mock_open_blank(); + npages = (size_t)banks * blocks * ppb; + + snprintf(path, sizeof(path), "%s/meta.bin", dir); + if (!(f = fopen(path, "rb")) || fread(mock_meta, 16, npages, f) != npages) + return -1; + fclose(f); + snprintf(path, sizeof(path), "%s/pages.bin", dir); + if (!(f = fopen(path, "rb"))) + return -1; + while (fread(rec, 4, 2, f) == 2) + { + g = (size_t)rec[0] * blocks * ppb + rec[1]; + if (g >= npages + || fread(mock_data + g * pagesize, 1, pagesize, f) != pagesize) + break; + } + fclose(f); + return 0; +} + +/* Open the medium a test names: a dump directory, or "blank:" and a preset + * geometry, formatted with ftl_hook_format(). The presets are rows of + * Apple's chip table, with the reserved count nand-nano3g.c derives. */ +int ftl_hook_open(const char *spec) +{ + static const struct + { + const char *name; + unsigned banks, blocks, pagesize, userblocks, mode, planes, layout; + } presets[] = + { + /* A514D3AD x 4, 4GB */ + { "2k4", 4, 4096, 2048, 3872, 8, 2, NAND_LAYOUT_ADJACENT }, + /* A555D5AD x 4, 8GB */ + { "2k8g", 4, 8192, 2048, 7744, 8, 2, NAND_LAYOUT_ADJACENT }, + /* B614D5EC x 2, 4GB */ + { "4k2", 2, 4096, 4096, 3872, 8, 2, NAND_LAYOUT_ADJACENT }, + /* 2555D5EC x 4 and A5D5D589 x 4, 8GB */ + { "halves", 4, 8192, 2048, 7744, 9, 2, NAND_LAYOUT_HALVES }, + /* A5D5D589 x 2, 4GB */ + { "both", 2, 8192, 2048, 7744, 4, 4, NAND_LAYOUT_BOTH }, + /* B614D5EC x 4, 8GB */ + { "single4k", 4, 4096, 4096, 3872, 1, 1, NAND_LAYOUT_SINGLE }, + /* A585D598 x 4, 8GB */ + { "split13", 4, 8320, 2048, 7744, 13, 2, NAND_LAYOUT_SPLIT13 }, + }; + unsigned i; + + if (!strncmp(spec, "collected:", 10)) + return open_collected(spec + 10); + if (!strncmp(spec, "halves:", 7)) + { + mock_open_layout(spec + 7, "halves"); + return 0; + } + if (!strncmp(spec, "both:", 5)) + { + mock_open_layout(spec + 5, "both"); + return 0; + } + if (strncmp(spec, "blank:", 6)) + { + mock_open(spec); + return 0; + } + for (i = 0; i < ARRAYLEN(presets); i++) + if (!strcmp(spec + 6, presets[i].name)) + break; + if (i == ARRAYLEN(presets)) + return -1; + mock_geo.banks = presets[i].banks; + mock_geo.blocks = presets[i].blocks; + mock_geo.pagesperblock = 128; + mock_geo.planes = presets[i].planes; + mock_geo.userblocks = presets[i].userblocks; + mock_geo.vflspares = (presets[i].blocks - presets[i].userblocks) + / presets[i].planes - 23; + mock_geo.twoplane = presets[i].mode == 8; + mock_geo.pagesize = presets[i].pagesize; + mock_geo.mode = presets[i].mode; + mock_geo.layout = presets[i].layout; + mock_geo.validated = true; + mock_open_blank(); + return ftl_hook_format(); +} + +/* The unit a physical block belongs to under the mounted layout, spares + * included, for tests that order pages as a superblock does; -1 if none */ +int ftl_hook_block_unit(uint32_t block) +{ + static int16_t unit_of[8320]; + static const struct nand_geometry *built; + uint32_t u, v; + + if (!geo || block >= geo->blocks || geo->blocks > ARRAYLEN(unit_of)) + return -1; + if (built != geo) + { + for (v = 0; v < ARRAYLEN(unit_of); v++) + unit_of[v] = -1; + for (u = 0; u < geo->planes; u++) + for (v = 0; v < geo->blocks / geo->planes; v++) + unit_of[ftl_unit_block(u, v)] = u; + built = geo; + } + return unit_of[block]; +} + +/* A bad spare followed by a replaced block in unit 0's table, as a spare + * that fails its erase and the next remap leave it: mark the first free + * slot bad, then remap vblock onto the following one */ +int ftl_hook_bad_spare_then_remap(uint32_t bank, uint32_t vblock) +{ + struct ftl_vfl_cxt *cxt = &ftl_vfl_cxt[bank]; + uint32_t k; + + for (k = 0; k < geo->vflspares; k++) + if (cxt->remap[k] == FTL_VFL_FREE) + break; + if (k == geo->vflspares) + return -1; + cxt->remap[k] = FTL_VFL_BAD; + cxt->badcount++; + return ftl_vfl_remap_block(bank, 0, vblock); +} + +/* Put the block unit's vblock currently maps to on bank's pending-bad + * list and commit, as Apple's firmware does after a failed read or + * write; then erase it through the VFL, which must replace it first. + * Returns the pending count left and whether the block moved. */ +int ftl_hook_pending_erase(uint32_t bank, uint32_t unit, uint32_t vblock, + bool *moved) +{ + struct ftl_vfl_cxt *cxt = &ftl_vfl_cxt[bank]; + uint32_t before = ftl_vfl_phys_block(bank, unit, vblock); + + cxt->pending[cxt->pendingcount++] = before; + if (ftl_vfl_commit_cxt(bank) || ftl_vfl_erase(bank, unit, vblock)) + return -1; + *moved = ftl_vfl_phys_block(bank, unit, vblock) != before; + return cxt->pendingcount; +} diff --git a/utils/ipodnano3g/ftltest/ftl_hooks.h b/utils/ipodnano3g/ftltest/ftl_hooks.h new file mode 100644 index 0000000000..ba328095a3 --- /dev/null +++ b/utils/ipodnano3g/ftltest/ftl_hooks.h @@ -0,0 +1,26 @@ +/* Declarations for ftl_hooks.c; force-included into every test. */ +#ifndef FTL_HOOKS_H +#define FTL_HOOKS_H +#include +int ftl_vfl_pool_entry(uint32_t bank, uint32_t block); +int ftl_sb_locate(uint32_t sb, uint32_t v, uint32_t *bank, uint32_t *page); +int ftl_locate(uint32_t lpn, uint32_t *bank, uint32_t *page); +void ftl_global_spread(uint32_t *lowest, uint32_t *highest); +uint32_t ftl_pool_snapshot(uint16_t *sb, uint16_t *erasectr, uint32_t max); +void ftl_pool_state(uint32_t *freecount, uint32_t *fresh, uint32_t *nlogs); +int ftl_hook_write_merge(uint32_t first, uint32_t last, + const void *(*src)(uint32_t lpn, void *ctx), + void *ctx); +_Bool ftl_hook_writeable(void); +void ftl_hook_ctrl(uint16_t *sb); +#include +int ftl_hook_check(char *msg, size_t len); +void ftl_hook_ctrl_summary(void); +int ftl_hook_settle(void); +int ftl_hook_format(void); +int ftl_hook_open(const char *spec); +int ftl_hook_block_unit(uint32_t block); +int ftl_hook_bad_spare_then_remap(uint32_t bank, uint32_t vblock); +int ftl_hook_pending_erase(uint32_t bank, uint32_t unit, uint32_t vblock, + _Bool *moved); +#endif diff --git a/utils/ipodnano3g/ftltest/mock_nand.c b/utils/ipodnano3g/ftltest/mock_nand.c new file mode 100644 index 0000000000..cfc2f056ee --- /dev/null +++ b/utils/ipodnano3g/ftltest/mock_nand.c @@ -0,0 +1,793 @@ +/* Host mock of the Nano 3G NAND driver, backed by the raw dumps: + * n3g-dump/nand_data_4banks.bin page data, bank by bank + * n3g-dump/meta_all.bin 16 bytes per page: spare words 0..2 and + * the nand_read_page() result + * + * Both are mapped MAP_PRIVATE, so erase and program are real - they change + * what later reads return - but they are copy-on-write in RAM and never + * touch the files. The 4GB mapping costs only the pages actually written. + * + * The NAND rules modelled here, measured on the device over a whole 4GB + * unit (see ../RESULTS.md): programming only ever clears bits; an erased page + * reads all-0xff with all-0xff metadata and result NAND_ECC_CLEAN; every + * written page reads NAND_ECC_CORRECTED. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "nand-target.h" +#include "mock_nand.h" + +/* The geometry the FTL sees: the dump's, unless mock_open_blank() or + * mock_open_layout() set another */ +static uint32_t BANKS = 4, BLOCKS = 4096, PPB = 128, PAGESIZE = 2048; +#define NPAGES ((size_t)BANKS * BLOCKS * PPB) +#define METASZ 16 + +/* Address translation from the numbering the FTL uses onto the backing + * store. The store is laid out bank-major in SBLOCKS blocks per bank; for + * mock_open() and mock_open_blank() that is the identity, and for + * mock_open_layout() the store is the dump's mode 8 layout while the FTL + * sees another (see mock_layout_to_dump()). */ +#define XL_IDENTITY 0 +#define XL_HALVES 1 +#define XL_BOTH 2 +static int xlayout = XL_IDENTITY; +static uint32_t SBLOCKS = 4096; +#define DUMP_BANKS 4 +#define DUMP_BLOCKS 4096 + +static size_t store_page(uint32_t bank, uint32_t page); + +uint8_t *mock_data; +uint8_t *mock_meta; +unsigned long mock_reads; +unsigned long mock_read_runs, mock_read_run_pages; +unsigned long mock_writes; +unsigned long mock_writes_by_type[256]; +unsigned long mock_crash_after; +jmp_buf mock_crash_jmp; + +void *mock_crash_bt[32]; +int mock_crash_btn; + +int mock_crash_torn; + +static void set_rc(size_t g, int32_t rc); + +/* g: the page (program) or first page of the block (erase), is_erase */ +static void crash_point(size_t g, int is_erase) +{ + if (mock_crash_after && !--mock_crash_after) + { + mock_crash_btn = backtrace(mock_crash_bt, 32); + if (mock_crash_torn && !is_erase) + { + /* A program cut short: garbage that fails ECC */ + memset(mock_data + g * PAGESIZE, 0x5a, PAGESIZE); + memset(mock_meta + g * METASZ, 0x00, NAND_META_WORDS * 4); + set_rc(g, NAND_ECC_FAILED); + } + else if (mock_crash_torn && is_erase) + { + /* An erase cut short: the first half of the block erased, the + * rest unreadable */ + uint32_t p; + for (p = 0; p < PPB; p++) + { + if (p < PPB / 2) + { + memset(mock_data + (g + p) * PAGESIZE, 0xff, PAGESIZE); + memset(mock_meta + (g + p) * METASZ, 0xff, NAND_META_WORDS * 4); + set_rc(g + p, NAND_ECC_CLEAN); + } + else + set_rc(g + p, NAND_ECC_FAILED); + } + } + longjmp(mock_crash_jmp, 1); + } +} +unsigned long mock_erases; +unsigned long mock_reprogram; +unsigned long mock_nonsequential; + +struct nand_geometry mock_geo = { + .banks = 4, .blocks = 4096, .pagesperblock = 128, .planes = 2, + .userblocks = 3872, .vflspares = 89, .twoplane = true, .pagesize = 2048, + .mode = 8, .layout = NAND_LAYOUT_ADJACENT, .validated = true, +}; + +unsigned long mock_runs, mock_run_pages, mock_twoplane_runs, mock_twoplane_pages; +unsigned long mock_bad_twoplane; + +/* Injected bad blocks */ +#define MAX_BAD 32 +static struct { uint32_t bank, block; int how; } bad[MAX_BAD]; +static unsigned int nbad; +unsigned long mock_write_fails, mock_erase_fails; + +void mock_set_bad(uint32_t bank, uint32_t block, int how) +{ + if (nbad < MAX_BAD) + { + bad[nbad].bank = bank; + bad[nbad].block = block; + bad[nbad].how = how; + nbad++; + } +} + +void mock_clear_bad(void) +{ + nbad = 0; + mock_write_fails = mock_erase_fails = 0; +} + +static int is_bad(uint32_t bank, uint32_t block, int how) +{ + unsigned int i; + + for (i = 0; i < nbad; i++) + if (bad[i].bank == bank && bad[i].block == block + && (bad[i].how & how)) + return 1; + return 0; +} + +static uint8_t *map_file(const char *path, size_t want) +{ + int fd = open(path, O_RDONLY); + struct stat st; + void *p; + + if (fd < 0 || fstat(fd, &st) || (size_t)st.st_size != want) + { + fprintf(stderr, "%s: missing or wrong size\n", path); + exit(2); + } + /* MAP_PRIVATE + PROT_WRITE: writes are copy-on-write, the file is + * never modified. MAP_NORESERVE keeps the 4GB mapping from being + * charged against commit limits up front. */ + p = mmap(NULL, want, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_NORESERVE, fd, 0); + if (p == MAP_FAILED) + { + perror("mmap"); + exit(2); + } + close(fd); + return p; +} + +/* A blank medium of any geometry: every page erased. Anonymous zero pages + * read as erased (see nand_read_page), so only written pages cost memory. */ +void mock_open_blank(void) +{ + BANKS = mock_geo.banks; + BLOCKS = mock_geo.blocks; + PPB = mock_geo.pagesperblock; + PAGESIZE = mock_geo.pagesize; + xlayout = XL_IDENTITY; + SBLOCKS = BLOCKS; + mock_data = mmap(NULL, NPAGES * PAGESIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + mock_meta = mmap(NULL, NPAGES * METASZ, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + if (mock_data == MAP_FAILED || mock_meta == MAP_FAILED) + { + perror("mmap"); + exit(2); + } + mock_reset_stats(); +} + +void mock_open(const char *dir) +{ + char path[512]; + + xlayout = XL_IDENTITY; + SBLOCKS = BLOCKS; + + snprintf(path, sizeof(path), "%s/nand_data_4banks.bin", dir); + mock_data = map_file(path, NPAGES * PAGESIZE); + snprintf(path, sizeof(path), "%s/meta_all.bin", dir); + mock_meta = map_file(path, NPAGES * METASZ); + mock_reset_stats(); +} + +void mock_reset_stats(void) +{ + mock_reads = mock_writes = mock_erases = 0; + mock_read_runs = mock_read_run_pages = 0; + mock_reprogram = mock_nonsequential = 0; + mock_runs = mock_run_pages = mock_twoplane_runs = mock_twoplane_pages = 0; + mock_bad_twoplane = 0; + memset(mock_writes_by_type, 0, sizeof(mock_writes_by_type)); +} + +static int32_t page_rc(size_t g) +{ + int32_t rc; + memcpy(&rc, mock_meta + g * METASZ + 12, 4); + return rc; +} + +static void set_rc(size_t g, int32_t rc) +{ + memcpy(mock_meta + g * METASZ + 12, &rc, 4); +} + +/* An erased page is the one that reads NAND_ECC_CLEAN; every written page + * reads NAND_ECC_CORRECTED. */ +static int is_erased(size_t g) +{ + return page_rc(g) == NAND_ECC_CLEAN; +} + +int nand_read_page(uint32_t bank, uint32_t page, void *databuf, + uint32_t *meta) +{ + size_t g; + + if (bank >= BANKS || page >= BLOCKS * PPB) + return -1; + g = store_page(bank, page); + mock_reads++; + /* An erased page reads all 0xff, as every one in the dump does; a blank + * medium's untouched pages are zero-filled and read the same way */ + if (is_erased(g)) + { + memset(databuf, 0xff, PAGESIZE); + if (meta) + memset(meta, 0xff, NAND_META_WORDS * 4); + return NAND_ECC_CLEAN; + } + memcpy(databuf, mock_data + g * PAGESIZE, PAGESIZE); + if (meta) + memcpy(meta, mock_meta + g * METASZ, NAND_META_WORDS * 4); + return page_rc(g); +} + +int nand_read_pages(struct nand_read *r, unsigned int n) +{ + unsigned int i; + + if (!n) + return -1; + mock_read_runs++; + mock_read_run_pages += n; + for (i = 0; i < n; i++) + { + r[i].ecc = nand_read_page(r[i].bank, r[i].page, r[i].buf, r[i].meta); + if (r[i].ecc < 0) + return -1; + } + return 0; +} + +int nand_write_page(uint32_t bank, uint32_t page, const void *databuf, + const uint32_t *meta) +{ + size_t g; + const uint8_t *src = databuf; + uint8_t *dst; + uint32_t i; + + if (bank >= BANKS || page >= BLOCKS * PPB) + return -1; + g = store_page(bank, page); + crash_point(g, 0); + if (is_bad(bank, page / PPB, MOCK_FAIL_WRITE)) + { + mock_write_fails++; + return NAND_OP_FAILED; + } + mock_writes++; + mock_writes_by_type[(meta[2] >> 8) & 0xff]++; + + /* Programming a page that is not erased is undefined on real NAND; + * the FTL must never do it. Count it, then model what the chip does. */ + if (!is_erased(g)) + mock_reprogram++; + /* Pages within a block must be programmed in order on MLC. */ + if ((page % PPB) && is_erased(g - 1)) + mock_nonsequential++; + + /* Program only ever clears bits, from all 0xff if erased */ + if (is_erased(g)) + { + memset(mock_data + g * PAGESIZE, 0xff, PAGESIZE); + memset(mock_meta + g * METASZ, 0xff, NAND_META_WORDS * 4); + } + dst = mock_data + g * PAGESIZE; + for (i = 0; i < PAGESIZE; i++) + dst[i] &= src[i]; + dst = mock_meta + g * METASZ; + for (i = 0; i < NAND_META_WORDS * 4; i++) + dst[i] &= ((const uint8_t *)meta)[i]; + set_rc(g, NAND_ECC_CORRECTED); + return 0; +} + +/* A run is its pages one after another, as far as the medium can tell. A + * failing page stops the run there, which is one of the outcomes the driver + * allows ("any page of the run may not have been written"). Two-plane runs + * are checked for pairs the chip could actually take: plane 0 then plane 1 + * of the same bank, the same page, in a block and its twin. */ +int nand_write_pages(const struct nand_write *w, unsigned int n, + bool two_plane, uint32_t *failbank) +{ + unsigned int i; + int rc; + + if (!n || (two_plane && (n & 1))) + return -1; + mock_runs++; + mock_run_pages += n; + if (two_plane) + { + mock_twoplane_runs++; + mock_twoplane_pages += n; + for (i = 0; i < n; i += 2) + if (w[i].bank != w[i + 1].bank + || (w[i].page / PPB) % 2 != 0 + || w[i + 1].page != w[i].page + PPB) + mock_bad_twoplane++; + } + for (i = 0; i < n; i++) + { + rc = nand_write_page(w[i].bank, w[i].page, w[i].buf, w[i].meta); + if (rc) + { + if (failbank) + *failbank = w[i].bank; + return rc; + } + } + return 0; +} + +int nand_erase_block(uint32_t bank, uint32_t block) +{ + size_t g; + uint32_t p; + + if (bank >= BANKS || block >= BLOCKS) + return -1; + g = store_page(bank, block * PPB); + crash_point(g, 1); + if (is_bad(bank, block, MOCK_FAIL_ERASE)) + { + mock_erase_fails++; + return NAND_OP_FAILED; + } + mock_erases++; + memset(mock_data + g * PAGESIZE, 0xff, (size_t)PPB * PAGESIZE); + for (p = 0; p < PPB; p++) + { + memset(mock_meta + (g + p) * METASZ, 0xff, NAND_META_WORDS * 4); + set_rc(g + p, NAND_ECC_CLEAN); + } + return 0; +} + +int mock_snapshot(const char *dir) +{ + char path[512]; + int fd; + + snprintf(path, sizeof(path), "%s/nand_data_4banks.bin", dir); + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) + return -1; + if (write(fd, mock_data, NPAGES * PAGESIZE) + != (ssize_t)(NPAGES * PAGESIZE)) + { + close(fd); + return -1; + } + close(fd); + + snprintf(path, sizeof(path), "%s/meta_all.bin", dir); + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) + return -1; + if (write(fd, mock_meta, NPAGES * METASZ) != (ssize_t)(NPAGES * METASZ)) + { + close(fd); + return -1; + } + close(fd); + return 0; +} + +unsigned int nand_get_bank_count(void) +{ + return BANKS; +} + +const struct nand_geometry *nand_get_geometry(void) +{ + return &mock_geo; +} + +/* ---- Other layouts over the dump ---- + * + * The dump's chip is mode 8: VFL block v of unit u is physical block 2v+u, + * four banks of 4096 blocks. mock_open_layout() presents the same pages as a + * chip of another layout would hold them - each superblock's pages in the + * same order - by translating block numbers on every access. Only the VFL + * contexts must change, because they hold physical block numbers; they are + * rewritten in the copy-on-write mapping when the layout is opened. */ + +/* Apple formats its context ring within physical blocks 1..199, but the + * layout formulas put the dump's context block (block 1 of each bank) at + * 2048 or 4096. So each new bank swaps two blocks of the formula's + * numbering: new block 1 becomes the primary dump bank's block 1, and the + * block the formula put at 1 (halves: the same bank's block 2; both: the + * secondary bank's block 0) takes block 1's formula place. Dump blocks 0-4 + * are all remapped by the VFL, so no FTL data moves. */ +static uint32_t swap_cxt_block(uint32_t block) +{ + uint32_t other = xlayout == XL_HALVES ? DUMP_BLOCKS / 2 : DUMP_BLOCKS; + + if (xlayout == XL_IDENTITY) + return block; + return block == 1 ? other : block == other ? 1 : block; +} + +void mock_layout_to_dump(uint32_t bank, uint32_t block, + uint32_t *dbank, uint32_t *dblock) +{ + uint32_t u, v, hi, r; + + block = swap_cxt_block(block); + switch (xlayout) + { + case XL_HALVES: + u = block >= DUMP_BLOCKS / 2; + v = block - u * (DUMP_BLOCKS / 2); + *dbank = bank; + *dblock = 2 * v + u; + break; + case XL_BOTH: + hi = block >= DUMP_BLOCKS; + r = block - hi * DUMP_BLOCKS; + v = r / 2; + u = (r & 1) + 2 * hi; + *dbank = 2 * (u % 2) + bank; + *dblock = 2 * v + u / 2; + break; + default: + *dbank = bank; + *dblock = block; + break; + } +} + +/* The layout formula alone, without the context block swap: where the FTL + * computes a block to be. A VFL remap entry names a replaced block this way, + * whatever physically holds its (never read) contents. */ +static void formula_from_dump(uint32_t dbank, uint32_t dblock, + uint32_t *bank, uint32_t *block) +{ + uint32_t u, v = dblock / 2, plane = dblock % 2; + + switch (xlayout) + { + case XL_HALVES: + *bank = dbank; + *block = v + plane * (DUMP_BLOCKS / 2); + break; + case XL_BOTH: + u = 2 * plane + dbank / 2; + *bank = dbank % 2; + *block = 2 * v + (u & 1) + ((u & 2) ? DUMP_BLOCKS : 0); + break; + default: + *bank = dbank; + *block = dblock; + break; + } +} + +void mock_layout_from_dump(uint32_t dbank, uint32_t dblock, + uint32_t *bank, uint32_t *block) +{ + formula_from_dump(dbank, dblock, bank, block); + *block = swap_cxt_block(*block); +} + +/* The store index of a page in the numbering the FTL sees */ +static size_t store_page(uint32_t bank, uint32_t page) +{ + uint32_t dbank, dblock; + + if (xlayout == XL_IDENTITY) + return (size_t)bank * BLOCKS * PPB + page; + mock_layout_to_dump(bank, page / PPB, &dbank, &dblock); + return ((size_t)dbank * SBLOCKS + dblock) * PPB + page % PPB; +} + +#define CXT_SIZE 0x800 +#define CXT_USEDCOUNT 0x1c +#define CXT_REMAP 0x2c +#define CXT_REMAP_N 820 +#define CXT_RING 0x694 +#define CXT_CKSUM 0x7f8 +#define VFL_SPARES 89 +#define VFL_FREE 0xfff0 +#define VFL_BAD 0xffff + +static uint16_t get16(const uint8_t *p, size_t off) +{ + return p[off] | p[off + 1] << 8; +} + +static void put16(uint8_t *p, size_t off, uint16_t v) +{ + p[off] = v; + p[off + 1] = v >> 8; +} + +static uint32_t get32(const uint8_t *p, size_t off) +{ + return get16(p, off) | (uint32_t)get16(p, off + 2) << 16; +} + +static void cxt_checksum(const uint8_t *c, uint32_t *sum, uint32_t *x) +{ + size_t off; + + *sum = *x = 0xAABBCCDD; + for (off = 0; off < CXT_CKSUM; off += 4) + { + *sum += get32(c, off); + *x ^= get32(c, off); + } +} + +static int cxt_ok(const uint8_t *c) +{ + uint32_t sum, x; + + cxt_checksum(c, &sum, &x); + return sum == get32(c, CXT_CKSUM) && x == get32(c, CXT_CKSUM + 4); +} + +static void cxt_seal(uint8_t *c) +{ + uint32_t sum, x; + + cxt_checksum(c, &sum, &x); + put16(c, CXT_CKSUM, sum); + put16(c, CXT_CKSUM + 2, sum >> 16); + put16(c, CXT_CKSUM + 4, x); + put16(c, CXT_CKSUM + 6, x >> 16); +} + +static uint32_t store_word(size_t g, int w) +{ + uint32_t v; + memcpy(&v, mock_meta + g * METASZ + 4 * w, 4); + return v; +} + +/* Store page g holds a VFL context with good checksums, as the FTL's + * ftl_vfl_read_cxt() accepts one */ +static int store_is_cxt(size_t g) +{ + uint32_t w2 = store_word(g, 2); + + return !is_erased(g) && page_rc(g) >= 0 && page_rc(g) != NAND_ECC_FAILED + && ((w2 >> 8) & 0xff) == 0x80 && (w2 & 0xff) == 0 + && cxt_ok(mock_data + g * PAGESIZE); +} + +/* The first good context of the commit at page of a dump block, or -1 */ +static long store_read_cxt(uint32_t dbank, uint32_t dblock, uint32_t page) +{ + size_t base = ((size_t)dbank * SBLOCKS + dblock) * PPB; + uint32_t i; + + for (i = page; i < page + 8 && i < PPB; i++) + if (store_is_cxt(base + i)) + return base + i; + return -1; +} + +/* The newest VFL context of a dump bank, found as ftl_vfl_open_bank() does */ +static const uint8_t *store_newest_cxt(uint32_t dbank) +{ + uint16_t ring[4]; + uint32_t i, block, last, best = 4, bestusn = 0xffffffff, w0; + long g = -1; + + for (block = 1; block < 200; block++) + if ((g = store_read_cxt(dbank, block, 0)) >= 0) + break; + if (g < 0) + return NULL; + for (i = 0; i < 4; i++) + ring[i] = get16(mock_data + g * PAGESIZE, CXT_RING + 2 * i); + for (i = 0; i < 4; i++) + { + if (ring[i] >= SBLOCKS || (g = store_read_cxt(dbank, ring[i], 0)) < 0) + continue; + w0 = store_word(g, 0); + if (w0 && w0 <= bestusn) + { + bestusn = w0; + best = i; + } + } + if (best == 4) + return NULL; + block = ring[best]; + last = 0; + for (i = 8; i < PPB; i += 8) + { + if (store_read_cxt(dbank, block, i) < 0) + break; + last = i; + } + g = store_read_cxt(dbank, block, last); + return g < 0 ? NULL : mock_data + g * PAGESIZE; +} + +/* A dump bank's block number as the new layout numbers it: where it now + * sits (for the ring, which is scanned), or with remap where the formula + * puts it (for a remap entry) */ +static uint16_t new_block(uint32_t dbank, uint16_t dblock, int remap) +{ + uint32_t bank, block; + + if (remap) + formula_from_dump(dbank, dblock, &bank, &block); + else + mock_layout_from_dump(dbank, dblock, &bank, &block); + return block; +} + +/* halves: every context keeps its bank, so each page is rewritten alone */ +static void rewrite_halves(void) +{ + size_t g, npages = (size_t)DUMP_BANKS * SBLOCKS * PPB; + uint32_t dbank, i; + uint8_t *c; + uint16_t e; + + for (g = 0; g < npages; g++) + { + if (!store_is_cxt(g)) + continue; + dbank = g / ((size_t)SBLOCKS * PPB); + c = mock_data + g * PAGESIZE; + for (i = 0; i < 2 * VFL_SPARES; i++) + { + e = get16(c, CXT_REMAP + 2 * i); + if (e != VFL_FREE && e != VFL_BAD) + put16(c, CXT_REMAP + 2 * i, new_block(dbank, e, 1)); + } + for (i = 0; i < 4; i++) + { + e = get16(c, CXT_RING + 2 * i); + if (e < SBLOCKS) + put16(c, CXT_RING + 2 * i, new_block(dbank, e, 0)); + } + cxt_seal(c); + } +} + +/* both: each new bank takes units 0 and 2 from dump bank ce and units 1 and + * 3 from dump bank ce + 2. The newest contexts of the two are merged (unit + * tables from each, header from the higher usn, ring from ce), and the + * merge replaces every context page of both, so whichever copy a VFL reads - + * including the secondary bank's, which now sit outside the ring - agrees. */ +static void rewrite_both(void) +{ + static uint8_t merged[CXT_SIZE]; + const uint8_t *src[2]; + size_t g, base, bankpages = (size_t)SBLOCKS * PPB; + uint32_t ce, u, i, dbank, nbank, nblock, side; + uint16_t e; + + for (ce = 0; ce < 2; ce++) + { + src[0] = store_newest_cxt(ce); + src[1] = store_newest_cxt(ce + 2); + if (!src[0] || !src[1]) + { + fprintf(stderr, "mock_open_layout: no VFL context on bank %u\n", + src[0] ? ce + 2 : ce); + exit(2); + } + /* The header - usn, FTL control blocks, update count and the rest - + * from the newer of the two, as the FTL takes its control blocks + * from the highest usn of any bank */ + memcpy(merged, src[get32(src[1], 0) > get32(src[0], 0)], CXT_SIZE); + memset(merged + CXT_REMAP, 0, 2 * CXT_REMAP_N); + for (u = 0; u < 4; u++) + { + const uint8_t *s = src[u % 2]; + uint32_t plane = u / 2; + + dbank = 2 * (u % 2) + ce; + put16(merged, CXT_USEDCOUNT + 2 * u, + get16(s, CXT_USEDCOUNT + 2 * plane)); + for (i = 0; i < VFL_SPARES; i++) + { + e = get16(s, CXT_REMAP + 2 * (plane * VFL_SPARES + i)); + if (e != VFL_FREE && e != VFL_BAD) + { + formula_from_dump(dbank, e, &nbank, &nblock); + e = nblock; + } + put16(merged, CXT_REMAP + 2 * (u * VFL_SPARES + i), e); + } + } + for (i = 0; i < 4; i++) + { + /* The ring is the primary's, which is where it sits */ + e = get16(src[0], CXT_RING + 2 * i); + put16(merged, CXT_RING + 2 * i, + e < SBLOCKS ? new_block(ce, e, 0) : e); + } + cxt_seal(merged); + + for (side = 0; side < 2; side++) + { + base = (ce + 2 * side) * bankpages; + for (g = base; g < base + bankpages; g++) + if (store_is_cxt(g)) + memcpy(mock_data + g * PAGESIZE, merged, CXT_SIZE); + } + } +} + +void mock_open_layout(const char *dir, const char *layout) +{ + int l = !strcmp(layout, "halves") ? XL_HALVES + : !strcmp(layout, "both") ? XL_BOTH : -1; + + if (l < 0) + { + fprintf(stderr, "mock_open_layout: unknown layout %s\n", layout); + exit(2); + } + /* Map and rewrite in the dump's own numbering */ + BANKS = DUMP_BANKS; + BLOCKS = DUMP_BLOCKS; + PPB = 128; + PAGESIZE = 2048; + mock_open(dir); + xlayout = l; + if (l == XL_HALVES) + rewrite_halves(); + else + rewrite_both(); + + if (l == XL_HALVES) + mock_geo = (struct nand_geometry){ + .banks = 4, .blocks = 4096, .pagesperblock = 128, .planes = 2, + .userblocks = 3872, .vflspares = 89, .twoplane = false, + .pagesize = 2048, .mode = 9, .layout = NAND_LAYOUT_HALVES, + .validated = true, + }; + else + mock_geo = (struct nand_geometry){ + .banks = 2, .blocks = 8192, .pagesperblock = 128, .planes = 4, + .userblocks = 7744, .vflspares = 89, .twoplane = false, + .pagesize = 2048, .mode = 4, .layout = NAND_LAYOUT_BOTH, + .validated = true, + }; + BANKS = mock_geo.banks; + BLOCKS = mock_geo.blocks; +} diff --git a/utils/ipodnano3g/ftltest/mock_nand.h b/utils/ipodnano3g/ftltest/mock_nand.h new file mode 100644 index 0000000000..72001495cb --- /dev/null +++ b/utils/ipodnano3g/ftltest/mock_nand.h @@ -0,0 +1,85 @@ +/* Host mock of the Nano 3G NAND driver. */ +#ifndef MOCK_NAND_H +#define MOCK_NAND_H + +#include + +/* The backing store, mapped copy-on-write: writes land in RAM and never + * reach the dump files. Page data is NAND_PAGE_SIZE per page; meta is 16 + * bytes per page - 3 spare words then the int32 nand_read_page() result. */ +extern uint8_t *mock_data; + +/* What nand_get_geometry() returns: the dumped 4GB unit's chip, which tests + * may alter before ftl_init() */ +struct nand_geometry; +extern struct nand_geometry mock_geo; +extern uint8_t *mock_meta; + +/* Counters, for tests to assert on. Reset with mock_reset_stats(). */ +extern unsigned long mock_reads; +extern unsigned long mock_read_runs, mock_read_run_pages; +extern unsigned long mock_writes; +extern unsigned long mock_writes_by_type[256]; /* by spare type */ +extern unsigned long mock_erases; +/* Violations of real NAND's rules, which the FTL must never commit: */ +extern unsigned long mock_reprogram; /* programmed a non-erased page */ +extern unsigned long mock_nonsequential; /* skipped a page within a block */ +extern unsigned long mock_runs, mock_run_pages; /* nand_write_pages calls */ +extern unsigned long mock_twoplane_runs, mock_twoplane_pages; +extern unsigned long mock_bad_twoplane; /* a pair the chip could not take */ + +/* Fault injection: make a physical block refuse programs and/or erases, as a + * worn-out block does. NAND_OP_FAILED is what the real driver returns when + * the chip reports a failure. */ +#define MOCK_FAIL_WRITE 1 +#define MOCK_FAIL_ERASE 2 +void mock_set_bad(uint32_t bank, uint32_t block, int how); +void mock_clear_bad(void); +extern unsigned long mock_write_fails, mock_erase_fails; + +/* Crash injection: after this many more programs and erases, the next one + * does not happen and longjmp(mock_crash_jmp, 1) is taken instead, as if + * power were cut just before it. 0 disables. */ +#include +extern unsigned long mock_crash_after; +extern jmp_buf mock_crash_jmp; +extern void *mock_crash_bt[32]; /* where the last cut happened */ +extern int mock_crash_btn; +extern int mock_crash_torn; /* leave the cut operation half done */ + +void mock_open(const char *dir); +/* A blank, erased medium with mock_geo's geometry, for ftl_hook_format() */ +void mock_open_blank(void); +void mock_reset_stats(void); + +/* The dump presented as a chip of another layout would hold it: "halves" + * (mode 9: 4 banks of 4096 blocks, unit u of VFL block v at v + u * 2048) or + * "both" (mode 4: 2 banks of 8192 blocks, 4 units, unit u of v at + * 2v + (u & 1) + ((u & 2) ? 4096 : 0), taking dump banks ce and ce + 2). + * Every superblock keeps its pages in order, except that on each new bank + * block 1 and the formula's place for the primary dump bank's block 1 (2048 + * or 4096) are swapped, so the context ring starts within blocks 1..199 as + * on a chip Apple formatted. The rewritten rings name blocks where they now + * sit; remap entries name replaced blocks by the formula alone, as the FTL + * computes them (their contents are never read). Sets mock_geo, and every NAND + * call, mock_set_bad() and crash injection then use the new numbering. The + * VFL contexts are rewritten in the copy-on-write mapping to hold the new + * block numbers; for "both" the newest contexts of the two dump banks are + * merged into every context page of both. mock_data and mock_meta stay in + * the dump's layout. */ +void mock_open_layout(const char *dir, const char *layout); + +/* Block translation of the layout last opened, the identity after + * mock_open() or mock_open_blank(): a block as the FTL numbers it to the + * dump's bank and block holding its pages, and back */ +void mock_layout_to_dump(uint32_t bank, uint32_t block, + uint32_t *dbank, uint32_t *dblock); +void mock_layout_from_dump(uint32_t dbank, uint32_t dblock, + uint32_t *bank, uint32_t *block); + +/* Write the current state of the mock out, for post-mortem inspection. The + * files are the backing store, so after mock_open_layout() they are in the + * dump's layout, not the one the FTL saw. Returns 0, or -1 with errno set. */ +int mock_snapshot(const char *dir); + +#endif diff --git a/utils/ipodnano3g/ftltest/replay.c b/utils/ipodnano3g/ftltest/replay.c new file mode 100644 index 0000000000..ea0b9db902 --- /dev/null +++ b/utils/ipodnano3g/ftltest/replay.c @@ -0,0 +1,140 @@ +/* Replay a block-layer write trace through the FTL on the mock NAND and + * report what it cost. The trace is Linux's block_rq_issue tracepoint from a + * guest writing to a disk with 4096-byte logical sectors, so each 512-byte + * trace sector is a quarter of an FTL sector. Writes are split as Rockbox's + * USB storage does, into calls of at most WRITE_BUFFER_SIZE (24 KB). + * + * replay TRACE [DUMPDIR] + */ +#include +#include +#include +#include "nand-target.h" +#include "ftl-target.h" +#include "mock_nand.h" + +#define CHUNK_SECTORS (24 * 1024 / NAND_PAGE_SIZE) /* 11: 24 KB, whole */ +#define SEC_PER_4K 2 + +/* Counters an instrumented FTL build may provide */ +extern unsigned long st_merge __attribute__((weak)), + st_merge_wear __attribute__((weak)), st_merge_full __attribute__((weak)), + st_seqadopt __attribute__((weak)), st_seqfill __attribute__((weak)), + st_sync __attribute__((weak)), st_bound_logs __attribute__((weak)), + st_bound_pool __attribute__((weak)), st_append __attribute__((weak)), + st_log_alloc __attribute__((weak)); + +static void report(const char *phase, unsigned long hostpages, + unsigned long calls) +{ + unsigned long data = mock_writes_by_type[0x40] + mock_writes_by_type[0x41]; + unsigned long ctrl = mock_writes - data; + + printf("%-8s host pages %7lu in %6lu calls | programs %7lu " + "(data %7lu = %.2fx host, ctrl %5lu) reads %7lu erases %5lu | " + "commits %lu, marks %lu\n", + phase, hostpages, calls, mock_writes, data, + hostpages ? (double)data / hostpages : 0.0, ctrl, mock_reads, + mock_erases, mock_writes_by_type[0x43], mock_writes_by_type[0x4f]); + printf(" program runs %lu (%.1f pages each), two-plane %lu " + "(%.0f%% of pages), bad pairs %lu\n", mock_runs, + mock_runs ? (double)mock_run_pages / mock_runs : 0.0, + mock_twoplane_runs, + mock_run_pages ? 100.0 * mock_twoplane_pages / mock_run_pages : 0.0, + mock_bad_twoplane); + printf(" read runs %lu (%.1f pages each)\n", mock_read_runs, + mock_read_runs ? (double)mock_read_run_pages / mock_read_runs : 0.0); + if (&st_merge) + { + printf(" appends %lu, logs opened %lu | syncs %lu (bound: logs %lu, " + "pool %lu) | merges %lu (whole-block %lu, wear %lu, folds %lu) " + "| sequential adoptions %lu filling %lu pages\n", + st_append, st_log_alloc, st_sync, st_bound_logs, st_bound_pool, + st_merge, st_merge_full, st_merge_wear, + st_merge - st_merge_full - st_merge_wear, st_seqadopt, st_seqfill); + st_append = st_log_alloc = st_sync = st_bound_logs = st_bound_pool = 0; + st_merge = st_merge_full = st_merge_wear = st_seqadopt = st_seqfill = 0; + } +} + +int main(int argc, char **argv) +{ + static uint8_t buf[32 * NAND_PAGE_SIZE]; + char line[512]; + FILE *f; + unsigned long hostpages = 0, calls = 0, reqs = 0; + const char *phase = "copy"; + + if (argc < 2) + { + fprintf(stderr, "usage: replay TRACE [DUMPDIR]\n"); + return 2; + } + f = fopen(argv[1], "r"); + if (!f) + { + perror(argv[1]); + return 1; + } + mock_open(argc > 2 ? argv[2] : "../n3g-dump"); + if (ftl_init() || ftl_sync() || ftl_init()) + { + puts("mount/settle failed"); + return 1; + } + mock_reset_stats(); + memset(buf, 0xa5, sizeof(buf)); + + while (fgets(line, sizeof(line), f)) + { + char *p, rwbs[8]; + unsigned long long sector; + unsigned long nr, bytes; + uint32_t lpn, n, done; + + if (strstr(line, "MARK COPY_DONE")) + { + if (ftl_sync()) + puts("sync failed"); + report(phase, hostpages, calls); + mock_reset_stats(); + hostpages = calls = 0; + phase = "delete"; + continue; + } + if (strstr(line, "MARK DELETE_DONE")) + break; + p = strstr(line, "block_rq_issue: "); + if (!p) + continue; + if (sscanf(p, "block_rq_issue: %*s %7s %lu () %llu + %lu", + rwbs, &bytes, §or, &nr) != 4) + continue; + if (!strchr(rwbs, 'W') || !nr) + continue; + reqs++; + lpn = sector / 4; + n = nr / 4; + for (done = 0; done < n; done += CHUNK_SECTORS / SEC_PER_4K * SEC_PER_4K) + { + uint32_t c = n - done; + + if (c > CHUNK_SECTORS / SEC_PER_4K * SEC_PER_4K) + c = CHUNK_SECTORS / SEC_PER_4K * SEC_PER_4K; + if (ftl_write(lpn + done, c, buf)) + { + printf("ftl_write(%u, %u) failed\n", lpn + done, c); + return 1; + } + calls++; + hostpages += c; + } + } + if (ftl_sync()) + puts("final sync failed"); + report(phase, hostpages, calls); + printf("%lu write requests; NAND rule violations: reprogram %lu, " + "nonsequential %lu\n", reqs, mock_reprogram, mock_nonsequential); + fclose(f); + return 0; +} diff --git a/utils/ipodnano3g/ftltest/stubs/config.h b/utils/ipodnano3g/ftltest/stubs/config.h new file mode 100644 index 0000000000..1ffee4ea3c --- /dev/null +++ b/utils/ipodnano3g/ftltest/stubs/config.h @@ -0,0 +1 @@ +/* Host test stub: ftl-nano3g.c needs nothing from config.h */ diff --git a/utils/ipodnano3g/ftltest/stubs/kernel.h b/utils/ipodnano3g/ftltest/stubs/kernel.h new file mode 100644 index 0000000000..515949ffa0 --- /dev/null +++ b/utils/ipodnano3g/ftltest/stubs/kernel.h @@ -0,0 +1,4 @@ +struct mutex { int locked; }; +static inline void mutex_init(struct mutex *m) { m->locked = 0; } +static inline void mutex_lock(struct mutex *m) { m->locked = 1; } +static inline void mutex_unlock(struct mutex *m) { m->locked = 0; } diff --git a/utils/ipodnano3g/ftltest/stubs/logf.h b/utils/ipodnano3g/ftltest/stubs/logf.h new file mode 100644 index 0000000000..3ffc310a67 --- /dev/null +++ b/utils/ipodnano3g/ftltest/stubs/logf.h @@ -0,0 +1,2 @@ +#include +#define logf(...) (fprintf(stderr, "logf: " __VA_ARGS__), fputc('\n', stderr)) diff --git a/utils/ipodnano3g/ftltest/stubs/panic.h b/utils/ipodnano3g/ftltest/stubs/panic.h new file mode 100644 index 0000000000..fd04ba2e3e --- /dev/null +++ b/utils/ipodnano3g/ftltest/stubs/panic.h @@ -0,0 +1,4 @@ +#include +#include +#define panicf(...) (fprintf(stderr, "panicf: " __VA_ARGS__), \ + fputc('\n', stderr), abort()) diff --git a/utils/ipodnano3g/ftltest/stubs/storage.h b/utils/ipodnano3g/ftltest/stubs/storage.h new file mode 100644 index 0000000000..c8a60e3edf --- /dev/null +++ b/utils/ipodnano3g/ftltest/stubs/storage.h @@ -0,0 +1 @@ +#define STORAGE_ALIGN_ATTR __attribute__((aligned(16))) diff --git a/utils/ipodnano3g/ftltest/stubs/system.h b/utils/ipodnano3g/ftltest/stubs/system.h new file mode 100644 index 0000000000..c34831d138 --- /dev/null +++ b/utils/ipodnano3g/ftltest/stubs/system.h @@ -0,0 +1,4 @@ +#ifndef MIN +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#endif +#define ARRAYLEN(a) (sizeof(a) / sizeof((a)[0])) diff --git a/utils/ipodnano3g/ftltest/test_badblock.c b/utils/ipodnano3g/ftltest/test_badblock.c new file mode 100644 index 0000000000..2dcb8df735 --- /dev/null +++ b/utils/ipodnano3g/ftltest/test_badblock.c @@ -0,0 +1,154 @@ +/* Bad-block handling: a physical block that refuses programs or erases must + * be replaced from the VFL's reserved spares, the write must still succeed, + * and the replacement must survive a remount - which only happens if the VFL + * context was committed. Ported alongside ftl-nano2g.c's VFL write side. + */ +#include +#include +#include +#include "nand-target.h" +#include "ftl-target.h" +#include "mock_nand.h" + +#define BANKS 4 +#define BLOCKS 4096 +#define PPB 128 +#define PLANES 2 +#define SBPAGES (PPB * PLANES * BANKS) +#define POOL 20 + +static int fails; + +static void check(const char *what, int ok) +{ + printf("%s %s\n", ok ? "PASS" : "FAIL", what); + if (!ok) + fails++; +} + +/* The physical (bank, block) behind page v of superblock sb */ +static int phys_of(uint32_t sb, uint32_t v, uint32_t *bank, uint32_t *block) +{ + uint32_t page; + + if (ftl_sb_locate(sb, v, bank, &page)) + return -1; + *block = page / PPB; + return 0; +} + +int main(int argc, char **argv) +{ + static uint8_t blk[SBPAGES * NAND_PAGE_SIZE]; /* one superblock */ + static uint8_t buf[SBPAGES * NAND_PAGE_SIZE]; + uint16_t psb[POOL], pec[POOL]; + uint32_t n, i, victim = 0, vlow = 0xffff, bank = 0, block = 0, lblock = 800; + uint32_t vblock; + int entry_before, entry_after; + + mock_open(argc > 1 ? argv[1] : "../n3g-dump"); + if (ftl_init()) { puts("mount failed"); return 1; } + check("mounts writeable", ftl_hook_writeable()); + check("settles", ftl_hook_settle() == 0); + + /* The next allocation takes the least-worn pool block, so that is the + * one a write will land in - make its first physical block refuse + * programs, as a worn-out block does. */ + n = ftl_pool_snapshot(psb, pec, POOL); + check("pool snapshot", n == POOL); + for (i = 0; i < n; i++) + if (pec[i] < vlow) { vlow = pec[i]; victim = psb[i]; } + check("found the block the next write will use", victim != 0); + check("located it physically", phys_of(victim, 0, &bank, &block) == 0); + vblock = victim * PLANES; /* v=0 is plane 0 of the superblock */ + entry_before = ftl_vfl_pool_entry(bank, block); + printf(" victim sb %u -> bank %u block %u (pool entry %d, ec %u)\n", + victim, bank, block, entry_before, vlow); + + mock_clear_bad(); + mock_set_bad(bank, block, MOCK_FAIL_WRITE); + + /* A whole-block write, so it goes through ftl_merge()'s retry path. + * A failed program into a live log panics by design instead. */ + for (i = 0; i < sizeof(blk); i++) + blk[i] = (uint8_t)(i * 7 + 3); + check("a write into a block that refuses programs still succeeds", + ftl_write(lblock * SBPAGES, SBPAGES, blk) == 0); + check("the mock really did refuse a program", mock_write_fails > 0); + check("the data reads back", + ftl_read(lblock * SBPAGES, SBPAGES, buf) == 0 + && !memcmp(buf, blk, sizeof(blk))); + + /* The bad virtual block must now be remapped onto a reserved spare */ + { + int found = -1; + uint32_t b; + + for (b = 0; b < BLOCKS; b++) + if (ftl_vfl_pool_entry(bank, b) == (int)vblock) + { + found = b; + break; + } + printf(" vblock %u is now served by physical block %d\n", + vblock, found); + check("the bad block was remapped onto a spare", found > 0); + } + + /* ...and the remap has to survive a remount, which means the VFL + * context was committed to flash */ + check("sync and remount", ftl_sync() == 0 && ftl_init() == 0); + { + int found = -1; + uint32_t b; + + for (b = 0; b < BLOCKS; b++) + if (ftl_vfl_pool_entry(bank, b) == (int)vblock) + { + found = b; + break; + } + check("the remap survived the remount (VFL context committed)", + found > 0); + } + entry_after = ftl_vfl_pool_entry(bank, block); + check("the old slot is unchanged in the pool table", + entry_after == entry_before); + check("the data still reads back after the remount", + ftl_read(lblock * SBPAGES, SBPAGES, buf) == 0 + && !memcmp(buf, blk, sizeof(blk))); + + /* A block that refuses erases must be replaced too. Erases happen when + * the *old* superblock is released after a merge, so the block to kill + * is the one that currently holds the logical block being written. */ + mock_clear_bad(); + { + uint32_t page2; + + check("locate the block about to be released", + ftl_locate((lblock + 1) * SBPAGES, &bank, &page2) == 0); + block = page2 / PPB; + printf(" lblock %u currently lives in bank %u block %u\n", + lblock + 1, bank, block); + mock_set_bad(bank, block, MOCK_FAIL_ERASE); + for (i = 0; i < sizeof(blk); i++) + blk[i] = (uint8_t)(i * 13 + 1); + /* The erase comes when the old superblock is released, which for a + * partial write happens at the sync that folds the log. */ + check("a write that must erase a dead block still succeeds", + ftl_write((lblock + 1) * SBPAGES, 16, blk) == 0 + && ftl_sync() == 0); + check("the mock really did refuse an erase", mock_erase_fails > 0); + check("that data reads back too", + ftl_read((lblock + 1) * SBPAGES, 16, buf) == 0 + && !memcmp(buf, blk, 16 * NAND_PAGE_SIZE)); + } + + mock_clear_bad(); + check("final sync and remount", ftl_sync() == 0 && ftl_init() == 0); + check("everything still readable", + ftl_read(lblock * SBPAGES, 16, buf) == 0); + + printf(fails ? "SOME CHECKS FAILED\n" : "ALL PASS\n"); + return fails ? 1 : 0; +} diff --git a/utils/ipodnano3g/ftltest/test_crash.c b/utils/ipodnano3g/ftltest/test_crash.c new file mode 100644 index 0000000000..7a7f5dbc83 --- /dev/null +++ b/utils/ipodnano3g/ftltest/test_crash.c @@ -0,0 +1,232 @@ +/* Power-loss stress: random writes and syncs over a group of logical blocks, + * with the mock cutting power at a random program or erase - in the middle + * of a log append, a fold, a sequential adoption, a wear-levelling move or a + * commit. After each cut the FTL is mounted afresh and every sector in the + * group must hold what the last acknowledged write put there; a sector in + * the write that was cut may hold either its old or its new contents. + * + * test_crash [DUMPDIR] [CUTS] [SEED] [MAXOPS] [TORN] + */ +#include +#include +#include +#include "nand-target.h" +#include "ftl-target.h" +#include "mock_nand.h" + +#define SBPAGES 1024 +#define BASE 1000 /* first logical block used */ +#define NLB 40 /* logical blocks used */ +#define NLPN (NLB * SBPAGES) +#define CHUNK 12 /* Rockbox's USB write size */ + +static uint32_t model[NLPN]; /* tag of each sector, 0 = unwritten */ +static uint32_t pend_lo, pend_n, pend_tag, pend_base; +static uint8_t page[CHUNK * NAND_PAGE_SIZE]; +static uint32_t rnd_state; + +static uint32_t rnd(void) +{ + rnd_state = rnd_state * 1103515245u + 12345u; + return rnd_state >> 8; +} + +static uint32_t tag_of(const uint8_t *p) +{ + uint32_t t; + memcpy(&t, p, 4); + return t; +} + +/* Write n sectors from lpn, tag = (write id << 12) + offset, in USB chunks */ +static int do_write(uint32_t lo, uint32_t n, uint32_t id) +{ + uint32_t done, k, c; + + pend_lo = lo; + pend_n = n; + pend_tag = id; + pend_base = lo; + for (done = 0; done < n; done += c) + { + c = n - done < CHUNK ? n - done : CHUNK; + for (k = 0; k < c; k++) + { + uint32_t t = (id << 12) + done + k, w; + for (w = 0; w < NAND_PAGE_SIZE; w += 4) + memcpy(page + k * NAND_PAGE_SIZE + w, &t, 4); + } + { + int rc = ftl_write(BASE * SBPAGES + lo + done, c, page); + if (rc) + { + printf(" ftl_write(+%u, %u) = %d\n", lo + done, c, rc); + return -1; + } + } + /* Acknowledged: this chunk is now owed to the host */ + for (k = 0; k < c; k++) + model[lo + done + k] = (id << 12) + done + k; + pend_lo = lo + done + c; + pend_n = n - done - c; + } + pend_n = 0; + return 0; +} + +static int verify(uint32_t *bad_out, uint32_t *ambig_out) +{ + static uint8_t buf[NAND_PAGE_SIZE]; + uint32_t i, got, bad = 0, ambig = 0; + + for (i = 0; i < NLPN; i++) + { + if (ftl_read(BASE * SBPAGES + i, 1, buf)) + { + bad++; + continue; + } + got = tag_of(buf); + if (model[i] == 0 && got == 0xffffffff) + got = 0; + if (got == model[i]) + continue; + if (pend_n && i >= pend_lo && i < pend_lo + pend_n + && got == (pend_tag << 12) + (i - pend_base)) + { + ambig++; + model[i] = got; + continue; + } + if (bad < 5) + printf(" lpn +%u: got %08x want %08x\n", i, got, model[i]); + bad++; + } + *bad_out = bad; + *ambig_out = ambig; + return bad ? -1 : 0; +} + +int main(int argc, char **argv) +{ + static unsigned long cuts, cut; + static uint32_t id = 1, bad, ambig, totalbad, syncs, writes; + static int fails; + + static unsigned long maxops; + + cuts = argc > 2 ? strtoul(argv[2], NULL, 0) : 300; + maxops = argc > 4 ? strtoul(argv[4], NULL, 0) : 6000; + mock_crash_torn = argc > 5 ? atoi(argv[5]) : 0; + + rnd_state = argc > 3 ? strtoul(argv[3], NULL, 0) : 12345; + if (ftl_hook_open(argc > 1 ? argv[1] : "../n3g-dump")) + { + puts("cannot open the medium"); + return 2; + } + if (ftl_init() || ftl_sync() || ftl_init()) + { + puts("mount failed"); + return 1; + } + /* Start from known contents: every sector of the group written once */ + { + uint32_t lb; + for (lb = 0; lb < NLB; lb++) + if (do_write(lb * SBPAGES, SBPAGES, id++)) + { + puts("initial fill failed"); + return 1; + } + if (ftl_sync()) + { + puts("initial sync failed"); + return 1; + } + } + + for (cut = 0; cut < cuts; cut++) + { + mock_crash_after = 1 + rnd() % maxops; + if (!setjmp(mock_crash_jmp)) + { + for (;;) + { + uint32_t r = rnd() % 100, lb = rnd() % NLB; + + if (r < 75) /* scattered small write */ + { + uint32_t n = 1 + rnd() % 40; + uint32_t lo = lb * SBPAGES + rnd() % (SBPAGES - n); + if (do_write(lo, n, id++)) break; + writes++; + } + else if (r < 90) /* a stretch written in order */ + { + uint32_t n = 64 + rnd() % 900; + uint32_t lo = lb * SBPAGES + rnd() % (SBPAGES - n); + if (do_write(lo, n, id++)) break; + writes++; + } + else if (r < 95) /* a whole block */ + { + if (do_write(lb * SBPAGES, SBPAGES, id++)) break; + writes++; + } + else + { + { int rc = ftl_sync(); if (rc) { printf(" ftl_sync() = %d\n", rc); break; } } + syncs++; + } + } + printf(" cut %lu: a write or sync failed without a cut\n", cut); + fails++; + break; + } + /* Power was cut. Mount afresh and check. */ + mock_crash_after = 0; + if (ftl_init()) + { + printf(" cut %lu: mount failed\n", cut); + fails++; + break; + } + { + char msg[256]; + if (ftl_hook_check(msg, sizeof(msg))) + { + int k; + printf(" cut %lu: FTL state inconsistent after remount: %s\n", cut, msg); + printf(" the cut was at:"); + for (k = 0; k < mock_crash_btn; k++) + printf(" %p", mock_crash_bt[k]); + printf("\n"); + fails++; + break; + } + } + verify(&bad, &ambig); + if (bad) + { + int k; + printf(" cut %lu: %u sectors wrong; pending +%u n %u tag %u\n", cut, bad, pend_base, pend_n, pend_tag); + printf(" the cut was at:"); + for (k = 0; k < mock_crash_btn; k++) + printf(" %p", mock_crash_bt[k]); + printf("\n"); + totalbad += bad; + fails++; + break; + } + pend_n = 0; + } + printf(" %lu cuts, %u writes, %u syncs, %u sectors wrong\n", + cut, writes, syncs, totalbad); + printf("%s every acknowledged write survives every cut\n", + fails ? "FAIL" : "PASS"); + printf("NAND rules: reprogram %lu, nonsequential %lu\n", + mock_reprogram, mock_nonsequential); + printf(fails ? "SOME CHECKS FAILED\n" : "ALL PASS\n"); + return fails ? 1 : 0; +} diff --git a/utils/ipodnano3g/ftltest/test_eligible.c b/utils/ipodnano3g/ftltest/test_eligible.c new file mode 100644 index 0000000000..c9690f7f2e --- /dev/null +++ b/utils/ipodnano3g/ftltest/test_eligible.c @@ -0,0 +1,118 @@ +/* Unclean-stop safety of pool allocation: a block released since the last + * commit is still the committed map's copy of some logical block, so + * ftl_pool_alloc() must never have to hand one out. ftl_write_merge() + * commits early to guarantee that; its bound has to count open logs too, + * since they occupy pool blocks. The write sequence below drives the pool + * to "no block free at the last commit" without tripping a bound that + * ignores ftl_nlogs. + */ +#include +#include +#include +#include "nand-target.h" +#include "ftl-target.h" +#include "mock_nand.h" + +#define SBPAGES 1024 +#define BASE 1000 /* first logical block used */ + +static int fails; + +static void check(const char *what, int ok) +{ + printf("%s %s\n", ok ? "PASS" : "FAIL", what); + if (!ok) + fails++; +} + +static void fill(uint8_t *buf, uint32_t lpn, uint32_t n, uint8_t salt) +{ + uint32_t i, j; + + for (i = 0; i < n; i++) + for (j = 0; j < NAND_PAGE_SIZE; j++) + buf[i * NAND_PAGE_SIZE + j] = (uint8_t)(lpn + i + j * 3 + salt); +} + +/* Blocks the next allocation may take without breaking the invariant */ +static uint32_t safe_free(void) +{ + uint32_t freecount, fresh, nlogs; + + ftl_pool_state(&freecount, &fresh, &nlogs); + printf(" free %u fresh %u logs %u\n", freecount, fresh, nlogs); + return freecount > fresh ? freecount - fresh : 0; +} + +int main(int argc, char **argv) +{ + static uint8_t blk[SBPAGES * NAND_PAGE_SIZE], buf[SBPAGES * NAND_PAGE_SIZE]; + uint32_t i, lpn, minsafe = 0xffffffff, bad = 0; + int ok = 1; + + mock_open(argc > 1 ? argv[1] : "../n3g-dump"); + if (ftl_init()) { puts("mount failed"); return 1; } + check("mounts writeable", ftl_hook_writeable()); + check("settles", ftl_hook_settle() == 0); + + /* Eight blocks that each get a log and then a whole-block write, which + * folds the log: two releases per block */ + for (i = 0; i < 8 && ok; i++) + { + lpn = (BASE + i) * SBPAGES; + fill(blk, lpn + 5, 16, 1); + ok = ftl_write(lpn + 5, 16, blk) == 0; + fill(blk, lpn, SBPAGES, 2); + ok = ok && ftl_write(lpn, SBPAGES, blk) == 0; + } + check("eight log-then-whole-block rewrites", ok); + + /* One log filled in order over two calls, adopted outright: one release */ + lpn = (BASE + 8) * SBPAGES; + fill(blk, lpn, SBPAGES, 3); + check("a sequential log filled in two halves", + ftl_write(lpn, SBPAGES / 2, blk) == 0 + && ftl_write(lpn + SBPAGES / 2, SBPAGES / 2, + blk + SBPAGES / 2 * NAND_PAGE_SIZE) == 0); + + /* Open logs on four more blocks. Each takes a pool block. */ + for (i = 9; i < 13 && ok; i++) + { + uint32_t s = safe_free(); + + if (s < minsafe) + minsafe = s; + lpn = (BASE + i) * SBPAGES; + fill(blk, lpn + 7, 16, 4); + ok = ftl_write(lpn + 7, 16, blk) == 0; + } + check("four small writes opening logs", ok); + printf(" fewest safe blocks before an allocation: %u\n", minsafe); + + /* And an unclean stop now must lose nothing */ + check("remount without syncing", ftl_init() == 0); + for (i = 0; i < 8; i++) + { + lpn = (BASE + i) * SBPAGES; + fill(blk, lpn, SBPAGES, 2); + if (ftl_read(lpn, SBPAGES, buf) || memcmp(buf, blk, sizeof(blk))) + bad++; + } + lpn = (BASE + 8) * SBPAGES; + fill(blk, lpn, SBPAGES, 3); + if (ftl_read(lpn, SBPAGES, buf) || memcmp(buf, blk, sizeof(blk))) + bad++; + for (i = 9; i < 13; i++) + { + lpn = (BASE + i) * SBPAGES; + fill(blk, lpn + 7, 16, 4); + if (ftl_read(lpn + 7, 16, buf) || memcmp(buf, blk, 16 * NAND_PAGE_SIZE)) + bad++; + } + printf(" %u of 13 blocks wrong after the unclean remount\n", bad); + check("everything written reads back after an unclean stop", bad == 0); + + check("final sync", ftl_sync() == 0); + printf(fails ? "SOME CHECKS FAILED\n" : "ALL PASS\n"); + return fails ? 1 : 0; +} diff --git a/utils/ipodnano3g/ftltest/test_format.c b/utils/ipodnano3g/ftltest/test_format.c new file mode 100644 index 0000000000..186f4bc9b0 --- /dev/null +++ b/utils/ipodnano3g/ftltest/test_format.c @@ -0,0 +1,239 @@ +/* The FTL on a formatted blank medium of a chosen geometry, for chips no + * dump exists of - above all 4KiB pages, where one NAND page holds two + * sectors. Writes of whole superblocks, odd sectors and pages split across + * two calls must read back, before and after a sync and remount, and after + * a remount without one; the medium must never be programmed against NAND's + * rules. + * + * test_format blank:PRESET (see ftl_hook_open) + */ +#include +#include +#include +#include "nand-target.h" +#include "ftl-target.h" +#include "mock_nand.h" + +#define LBSECTORS 1024 /* sectors in a logical block, every preset */ +#define BASE 900 /* first logical block used */ +#define NLB 8 +#define NSEC (NLB * LBSECTORS) + +static uint16_t tag[NSEC]; /* what each sector holds; 0 = never written */ +static uint8_t buf[LBSECTORS * NAND_PAGE_SIZE]; +static int fails; + +static void check(const char *what, int ok) +{ + printf("%s %s\n", ok ? "PASS" : "FAIL", what); + if (!ok) + fails++; +} + +static void fill(uint8_t *p, uint32_t sector, uint16_t t) +{ + uint32_t j; + + for (j = 0; j < NAND_PAGE_SIZE; j += 4) + { + uint32_t w = sector * 2654435761u ^ t * 40503u ^ j; + memcpy(p + j, &w, 4); + } +} + +static int put(uint32_t off, uint32_t n, uint16_t t) +{ + uint32_t i; + + for (i = 0; i < n; i++) + fill(buf + i * NAND_PAGE_SIZE, BASE * LBSECTORS + off + i, t); + if (ftl_write(BASE * LBSECTORS + off, n, buf)) + return -1; + for (i = 0; i < n; i++) + tag[off + i] = t; + return 0; +} + +/* Every sector of the group, one read call per logical block */ +static int verify(void) +{ + static uint8_t want[NAND_PAGE_SIZE]; + uint32_t lb, i, bad = 0; + + for (lb = 0; lb < NLB; lb++) + { + if (ftl_read((BASE + lb) * LBSECTORS, LBSECTORS, buf)) + return -1; + for (i = 0; i < LBSECTORS; i++) + { + uint32_t s = lb * LBSECTORS + i; + + if (tag[s]) + fill(want, BASE * LBSECTORS + s, tag[s]); + else + memset(want, 0xff, NAND_PAGE_SIZE); + if (memcmp(buf + i * NAND_PAGE_SIZE, want, NAND_PAGE_SIZE)) + bad++; + } + } + if (bad) + printf(" %u sectors wrong\n", bad); + return bad ? -1 : 0; +} + +/* Single sectors one call at a time, and odd runs, read back singly */ +static int verify_single(uint32_t off, uint32_t n) +{ + static uint8_t one[3 * NAND_PAGE_SIZE], want[NAND_PAGE_SIZE]; + uint32_t i, k, len; + + for (i = 0; i < n; i += len) + { + len = 1 + (i % 3); + if (i + len > n) + len = n - i; + if (ftl_read(BASE * LBSECTORS + off + i, len, one)) + return -1; + for (k = 0; k < len; k++) + { + uint32_t s = off + i + k; + if (tag[s]) + fill(want, BASE * LBSECTORS + s, tag[s]); + else + memset(want, 0xff, NAND_PAGE_SIZE); + if (memcmp(one + k * NAND_PAGE_SIZE, want, NAND_PAGE_SIZE)) + return -1; + } + } + return 0; +} + +int main(int argc, char **argv) +{ + const char *spec = argc > 1 ? argv[1] : "blank:4k2"; + char msg[256]; + uint32_t i, r = 12345; + uint16_t t = 1; + int ok; + + if (ftl_hook_open(spec)) + { + printf("FAIL format %s\n", spec); + return 1; + } + printf(" %s: %u banks, %u-byte pages\n", spec, mock_geo.banks, + mock_geo.pagesize); + check("format programs every page in order", + mock_nonsequential == 0 && mock_reprogram == 0); + mock_reset_stats(); + check("formatted volume mounts", ftl_init() == 0); + check("... writable", ftl_hook_writeable()); + check("... sector count", ftl_num_sectors() + == mock_geo.userblocks / mock_geo.planes * LBSECTORS); + check("unwritten sectors read erased", verify() == 0); + + check("whole logical block", put(0, LBSECTORS, t++) == 0); + check("one sector at the start of a page", put(LBSECTORS + 10, 1, t++) + == 0); + check("one sector at the end of a page", put(LBSECTORS + 21, 1, t++) + == 0); + check("an odd run crossing pages", put(LBSECTORS + 101, 13, t++) == 0); + check("an odd run crossing logical blocks", + put(2 * LBSECTORS - 7, 15, t++) == 0); + check("two halves of one page in two calls", + put(3 * LBSECTORS + 50, 1, t++) == 0 + && put(3 * LBSECTORS + 51, 1, t++) == 0); + check("reads back", verify() == 0); + check("reads back a sector at a time", verify_single(LBSECTORS, 200) == 0 + && verify_single(2 * LBSECTORS - 16, 40) == 0); + + /* Random small writes, enough to fill and merge logs */ + ok = 1; + for (i = 0; i < 3000 && ok; i++) + { + uint32_t n, off; + + r = r * 1103515245u + 12345u; + n = 1 + (r >> 16) % 12; + r = r * 1103515245u + 12345u; + off = (r >> 8) % (NSEC - n); + ok = put(off, n, t++) == 0; + } + check("3000 random writes of 1-12 sectors", ok); + check("... read back", verify() == 0); + check("sync", ftl_sync() == 0); + check("remount", ftl_init() == 0); + check("... read back", verify() == 0); + check("... consistent", ftl_hook_check(msg, sizeof(msg)) == 0); + + /* Writes left uncommitted are recovered by the restore */ + ok = 1; + for (i = 0; i < 500 && ok; i++) + { + uint32_t n, off; + + r = r * 1103515245u + 12345u; + n = 1 + (r >> 16) % 5; + r = r * 1103515245u + 12345u; + off = (r >> 8) % (NSEC - n); + ok = put(off, n, t++) == 0; + } + check("500 more writes, no sync", ok); + check("remount without sync", ftl_init() == 0); + check("... read back", verify() == 0); + check("... consistent", ftl_hook_check(msg, sizeof(msg)) == 0 + || (printf(" %s\n", msg), 0)); + + check("no page programmed twice", mock_reprogram == 0); + check("no page programmed out of order", mock_nonsequential == 0); + check("no two-plane pair the chip could not take", + mock_bad_twoplane == 0); + + /* A bad spare before a replaced block must still mount; superblock + * 1500 holds none of the data. */ + check("a bad spare then a remap commits", + ftl_hook_bad_spare_then_remap(0, 1500) == 0); + check("... and still mounts", ftl_init() == 0); + check("... and reads back", verify() == 0); + + /* Apple's pending-bad list: the next erase replaces the block */ + { + bool moved = false; + check("a pending block is replaced at its erase", + ftl_hook_pending_erase(1, 0, 1600, &moved) == 0 && moved); + /* now its spare is pending: replacing it must clear the entry */ + moved = false; + check("a pending spare is replaced and leaves the list", + ftl_hook_pending_erase(1, 0, 1600, &moved) == 0 && moved); + check("... and still mounts", ftl_init() == 0); + check("... and reads back", verify() == 0); + } + check("VFL commits program every page in order", + mock_nonsequential == 0 && mock_reprogram == 0); + + /* The policy that keeps unvalidated chips safe, in this layout: the + * mount reads but never programs or erases */ + { + const struct nand_geometry good = mock_geo; + unsigned long writes, erases; + static uint8_t one[NAND_PAGE_SIZE]; + + mock_geo.validated = false; + check("an unvalidated row mounts", ftl_init() == 0); + check("... read-only", !ftl_hook_writeable()); + check("... and reads", ftl_read(0, 1, one) == 0); + writes = mock_writes; + erases = mock_erases; + memset(one, 0x5a, sizeof(one)); + check("... refuses a write", ftl_write(0, 1, one) != 0); + check("... sync does nothing", ftl_sync() == 0); + check("... no program or erase reached the NAND", + mock_writes == writes && mock_erases == erases); + mock_geo = good; + check("the validated row mounts writable", + ftl_init() == 0 && ftl_hook_writeable()); + } + + puts(fails ? "SOME FAILED" : "ALL PASS"); + return fails != 0; +} diff --git a/utils/ipodnano3g/ftltest/test_ftl.c b/utils/ipodnano3g/ftltest/test_ftl.c new file mode 100644 index 0000000000..5366bdc28e --- /dev/null +++ b/utils/ipodnano3g/ftltest/test_ftl.c @@ -0,0 +1,191 @@ +/* Host test of ftl-nano3g.c against the raw dump of a 4GB Nano 3G, or any + * medium ftl_hook_open() can present (the dump in another layout, or data + * collected from a contributor's unit). + * + * The oracle uses nothing the FTL decodes (no map, no contexts, no VFL + * tables): for every logical page it takes, among all pages whose spare + * says "user data, this lpn", the newest - highest usn, then the latest + * position in its superblock. Every lpn the FTL resolves must land on the + * oracle's copy (or, where copies tie, on one with identical data). + */ +#include +#include +#include +#include +#include "nand-target.h" +#include "ftl-target.h" +#include "mock_nand.h" + +#define BANKS mock_geo.banks +#define BLOCKS mock_geo.blocks +#define PPB mock_geo.pagesperblock +#define PAGE mock_geo.pagesize +#define NPAGES ((size_t)BANKS * BLOCKS * PPB) + +static uint8_t pagebuf[NAND_MAX_PAGE_SIZE], otherbuf[NAND_MAX_PAGE_SIZE]; + +static void read_g(size_t g, uint8_t *buf) +{ + nand_read_page(g / ((size_t)BLOCKS * PPB), g % ((size_t)BLOCKS * PPB), + buf, NULL); +} + +static int fails; + +static void check(const char *what, int ok) +{ + printf("%s %s\n", ok ? "PASS" : "FAIL", what); + if (!ok) + fails++; +} + +static uint32_t meta_rec[NAND_META_WORDS]; + +static uint32_t meta_word(size_t g, int w) +{ + nand_read_page(g / ((size_t)BLOCKS * PPB), g % ((size_t)BLOCKS * PPB), + pagebuf, meta_rec); + return meta_rec[w]; +} + +static int meta_type_g(size_t g) +{ + return (meta_word(g, 2) >> 8) & 0xff; +} + +/* Position of a physical page in its superblock's write order */ +static uint32_t v_of(size_t g) +{ + uint32_t bank = g / ((size_t)BLOCKS * PPB); + uint32_t block = (g / PPB) % BLOCKS; + uint32_t page = g % PPB; + return page * BANKS * mock_geo.planes + + ftl_hook_block_unit(block) * BANKS + bank; +} + +int main(int argc, char **argv) +{ + const char *spec = argc > 1 ? argv[1] : "../n3g-dump"; + static uint8_t buf[16 * NAND_MAX_PAGE_SIZE] __attribute__((aligned(16))); + uint32_t nsec, npages, spp, lpn, bank, page; + unsigned long mount_reads; + bool realdata = strncmp(spec, "collected:", 10) != 0; + int rc; + + if (ftl_hook_open(spec)) + { + puts("cannot open the medium"); + return 2; + } + + rc = ftl_init(); + mount_reads = mock_reads; + printf("ftl_init() = %d after %lu page reads\n", rc, mount_reads); + check("mounts", rc == 0); + if (rc) + return 1; + nsec = ftl_num_sectors(); + spp = PAGE / NAND_PAGE_SIZE; + npages = nsec / spp; + check("capacity is the row's user superblocks", + npages == mock_geo.userblocks / mock_geo.planes * PPB + * mock_geo.planes * BANKS); + + /* Landmarks, where the data is real */ + if (realdata && nsec == 1936u * 1024) + { + rc = ftl_read(0, 1, buf); + check("lpn 0 reads and is an MBR (55aa at 0x1fe)", + rc == 0 && buf[0x1fe] == 0x55 && buf[0x1ff] == 0xaa); + rc = ftl_read(0x7e, 1, buf); + check("lpn 0x7e is the firmware partition banner", + rc == 0 && !memcmp(buf, "{{~~ /-----\\", 13)); + } + /* Where individual lpns physically live changes whenever Apple's + * firmware runs, so beyond these landmarks everything is checked + * against the oracle. */ + + /* The oracle */ + uint64_t *best = malloc(npages * sizeof(uint64_t)); + uint32_t *where = malloc(npages * sizeof(uint32_t)); + uint8_t *tie = calloc(npages, 1); + memset(best, 0, npages * sizeof(uint64_t)); + memset(where, 0xff, npages * sizeof(uint32_t)); + for (size_t g = 0; g < NPAGES; g++) + { + int t = meta_type_g(g); + if (t != 0x40 && t != 0x41) + continue; + uint32_t l = meta_rec[0]; + if (l >= npages) + continue; + uint64_t key = ((uint64_t)meta_rec[1] << 32) | (v_of(g) + 1); + if (key > best[l]) + { + best[l] = key; + where[l] = g; + tie[l] = 0; + } + else if (key == best[l]) + tie[l] = 1; + } + + unsigned long agree = 0, tieok = 0, unwritten = 0, bad = 0, shown = 0; + for (lpn = 0; lpn < npages; lpn++) + { + if (ftl_locate(lpn, &bank, &page)) + { + bad++; + continue; + } + size_t g = (size_t)bank * BLOCKS * PPB + page; + if (where[lpn] == 0xffffffff) + { + /* no copy anywhere: the FTL must point at an erased page */ + if (meta_type_g(g) == 0xff) + unwritten++; + else if (bad++, shown++ < 10) + printf(" lpn %#x: no copy exists, FTL gives %zu (type %#x)\n", + lpn, g, meta_type_g(g)); + } + else if (g == where[lpn]) + agree++; + else if (tie[lpn] && meta_word(g, 0) == lpn + && (memcpy(otherbuf, pagebuf, PAGE), + read_g(where[lpn], pagebuf), + !memcmp(otherbuf, pagebuf, PAGE))) + tieok++; + else if (bad++, shown++ < 10) + printf(" lpn %#x: FTL gives %zu (usn %#x v %u), oracle %u " + "(usn %#x v %u)\n", lpn, g, meta_word(g, 1), v_of(g), + where[lpn], meta_word(where[lpn], 1), v_of(where[lpn])); + } + printf("oracle: %lu agree, %lu on an identical tied copy, " + "%lu unwritten, %lu disagree\n", agree, tieok, unwritten, bad); + check("every lpn resolves to the oracle's newest copy", bad == 0); + + /* Bulk reads through ftl_read, data compared with the oracle's copy */ + unsigned long rerr = 0, derr = 0; + for (lpn = 0; lpn < npages; lpn += 16) + { + uint32_t n = npages - lpn < 16 ? npages - lpn : 16; + if (ftl_read(lpn * spp, n * spp, buf)) + { + rerr++; + continue; + } + for (uint32_t i = 0; i < n; i++) + if (where[lpn + i] != 0xffffffff + && (read_g(where[lpn + i], pagebuf), + memcmp(buf + i * PAGE, pagebuf, PAGE))) + derr++; + } + printf("ftl_read over all %u sectors: %lu failed calls, %lu pages differ\n", + nsec, rerr, derr); + check("ftl_read returns the oracle's data for every sector", + rerr == 0 && derr == 0); + check("read past the end fails", ftl_read(nsec, 1, buf) != 0); + + printf(fails ? "SOME CHECKS FAILED\n" : "ALL PASS\n"); + return fails ? 1 : 0; +} diff --git a/utils/ipodnano3g/ftltest/test_geometry.c b/utils/ipodnano3g/ftltest/test_geometry.c new file mode 100644 index 0000000000..7943b97183 --- /dev/null +++ b/utils/ipodnano3g/ftltest/test_geometry.c @@ -0,0 +1,62 @@ +/* The mount's chip checks: a row that is not validated mounts read-only, + * a row whose reserved-block count does not match the VFL context is + * refused, and so are unknown layouts and page sizes. */ +#include +#include +#include "nand-target.h" +#include "ftl-target.h" +#include "mock_nand.h" + +static int fails; + +static void check(const char *what, int ok) +{ + printf("%s %s\n", ok ? "PASS" : "FAIL", what); + if (!ok) + fails++; +} + +int main(int argc, char **argv) +{ + static uint8_t buf[4 * NAND_PAGE_SIZE], orig[4 * NAND_PAGE_SIZE]; + const struct nand_geometry good = mock_geo; + int rc; + + mock_open(argc > 1 ? argv[1] : "../n3g-dump"); + + mock_geo.validated = false; + check("unvalidated row mounts", ftl_init() == 0); + check("... read-only", !ftl_hook_writeable()); + check("... reads", ftl_read(1000 * 1024, 4, orig) == 0); + memset(buf, 0x5a, sizeof(buf)); + check("... refuses a write", ftl_write(1000 * 1024, 4, buf) != 0); + check("... sync does nothing", ftl_sync() == 0); + check("... no program or erase reached the NAND", + mock_writes == 0 && mock_erases == 0); + check("... data unchanged", ftl_read(1000 * 1024, 4, buf) == 0 + && !memcmp(buf, orig, sizeof(buf))); + mock_geo = good; + + mock_geo.vflspares = 90; + rc = ftl_init(); + printf(" vflspares 90: %d\n", rc); + check("one reserved block too many is refused", rc == -5); + mock_geo.vflspares = 88; + rc = ftl_init(); + printf(" vflspares 88: %d\n", rc); + check("one reserved block too few is refused", rc == -5); + mock_geo = good; + + mock_geo.layout = NAND_LAYOUT_UNKNOWN; + check("an unknown layout is refused", ftl_init() == -4); + mock_geo = good; + mock_geo.pagesize = 8192; + check("an unsupported page size is refused", ftl_init() == -4); + mock_geo = good; + + check("the real row mounts writable", + ftl_init() == 0 && ftl_hook_writeable()); + + puts(fails ? "SOME FAILED" : "ALL PASS"); + return fails != 0; +} diff --git a/utils/ipodnano3g/ftltest/test_mock.c b/utils/ipodnano3g/ftltest/test_mock.c new file mode 100644 index 0000000000..d5e2c36532 --- /dev/null +++ b/utils/ipodnano3g/ftltest/test_mock.c @@ -0,0 +1,141 @@ +/* Tests for the mock NAND itself: erase/program semantics, the violation + * counters, and - most important - that writes never reach the dump files. + * Everything downstream (the FTL write path) rests on these holding. + */ +#include +#include +#include +#include +#include "nand-target.h" +#include "mock_nand.h" + +#define BANKS 4 +#define BLOCKS 4096 +#define PPB 128 + +static int fails; +static const char *dumpdir; + +static void check(const char *what, int ok) +{ + printf("%s %s\n", ok ? "PASS" : "FAIL", what); + if (!ok) + fails++; +} + +static int all_bytes(const void *p, size_t n, uint8_t v) +{ + const uint8_t *b = p; + while (n--) + if (*b++ != v) + return 0; + return 1; +} + +/* Read straight from the file on disk, bypassing the mapping */ +static int file_page(uint32_t bank, uint32_t page, uint8_t *out) +{ + char path[512]; + size_t g = (size_t)bank * BLOCKS * PPB + page; + int fd, ok; + + snprintf(path, sizeof(path), "%s/nand_data_4banks.bin", dumpdir); + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + ok = pread(fd, out, NAND_PAGE_SIZE, (off_t)g * NAND_PAGE_SIZE) + == NAND_PAGE_SIZE; + close(fd); + return ok ? 0 : -1; +} + +int main(int argc, char **argv) +{ + static uint8_t buf[NAND_PAGE_SIZE], buf2[NAND_PAGE_SIZE]; + static uint8_t orig[NAND_PAGE_SIZE], pattern[NAND_PAGE_SIZE]; + uint32_t meta[NAND_META_WORDS], meta2[NAND_META_WORDS]; + uint32_t bank = 0, block = 0, base; + int rc, i; + + dumpdir = argc > 1 ? argv[1] : "../n3g-dump"; + mock_open(dumpdir); + + /* Find a block whose page 0 is written, to test erase against */ + for (block = 0; block < BLOCKS; block++) + if (nand_read_page(bank, block * PPB, buf, meta) == NAND_ECC_CORRECTED) + break; + check("found a written block to work on", block < BLOCKS); + if (block >= BLOCKS) + return 1; + base = block * PPB; + memcpy(orig, buf, NAND_PAGE_SIZE); + check("a written page reads CORRECTED and matches the dump", + !file_page(bank, base, buf2) && !memcmp(orig, buf2, NAND_PAGE_SIZE)); + + /* Erase */ + mock_reset_stats(); + check("erase returns 0", nand_erase_block(bank, block) == 0); + rc = nand_read_page(bank, base, buf, meta); + check("an erased page reads CLEAN, all 0xff, meta all 0xff", + rc == NAND_ECC_CLEAN && all_bytes(buf, NAND_PAGE_SIZE, 0xff) + && all_bytes(meta, sizeof(meta), 0xff)); + rc = nand_read_page(bank, base + PPB - 1, buf, meta); + check("erase covers the whole block", + rc == NAND_ECC_CLEAN && all_bytes(buf, NAND_PAGE_SIZE, 0xff)); + check("erase counted", mock_erases == 1); + + /* Program */ + for (i = 0; i < NAND_PAGE_SIZE; i++) + pattern[i] = (uint8_t)(i * 7 + 1); + meta[0] = 0x12345678; meta[1] = 0x9abcdef0; meta[2] = 0x0000ff40; + check("program returns 0", + nand_write_page(bank, base, pattern, meta) == 0); + rc = nand_read_page(bank, base, buf, meta2); + check("a programmed page reads back exactly, as CORRECTED", + rc == NAND_ECC_CORRECTED && !memcmp(buf, pattern, NAND_PAGE_SIZE) + && !memcmp(meta2, meta, sizeof(meta))); + check("clean program raises no violations", + mock_reprogram == 0 && mock_nonsequential == 0); + + /* Program only clears bits */ + memset(buf2, 0x0f, NAND_PAGE_SIZE); + check("reprogramming returns 0", + nand_write_page(bank, base, buf2, meta) == 0); + rc = nand_read_page(bank, base, buf, meta2); + for (i = 0; i < NAND_PAGE_SIZE; i++) + if (buf[i] != (uint8_t)(pattern[i] & 0x0f)) + break; + check("programming only clears bits (reads old AND new)", + i == NAND_PAGE_SIZE); + check("reprogramming a written page is counted", mock_reprogram == 1); + + /* Out-of-order programming within a block */ + mock_reset_stats(); + nand_erase_block(bank, block); + nand_write_page(bank, base + 2, pattern, meta); + check("skipping a page within a block is counted", + mock_nonsequential == 1); + mock_reset_stats(); + nand_erase_block(bank, block); + nand_write_page(bank, base, pattern, meta); + nand_write_page(bank, base + 1, pattern, meta); + check("sequential programming raises no violation", + mock_nonsequential == 0 && mock_reprogram == 0); + + /* Bounds */ + check("out-of-range bank fails", + nand_read_page(BANKS, 0, buf, NULL) == -1 + && nand_write_page(BANKS, 0, pattern, meta) == -1 + && nand_erase_block(BANKS, 0) == -1); + check("out-of-range page/block fails", + nand_read_page(0, BLOCKS * PPB, buf, NULL) == -1 + && nand_write_page(0, BLOCKS * PPB, pattern, meta) == -1 + && nand_erase_block(0, BLOCKS) == -1); + + /* The whole point: none of that reached the file */ + check("the dump file on disk is untouched", + !file_page(bank, base, buf2) && !memcmp(orig, buf2, NAND_PAGE_SIZE)); + + printf(fails ? "SOME CHECKS FAILED\n" : "ALL PASS\n"); + return fails ? 1 : 0; +} diff --git a/utils/ipodnano3g/ftltest/test_permute.c b/utils/ipodnano3g/ftltest/test_permute.c new file mode 100644 index 0000000000..f14b34c3c3 --- /dev/null +++ b/utils/ipodnano3g/ftltest/test_permute.c @@ -0,0 +1,805 @@ +/* Tests for mock_open_layout(): the dump presented as a mode 9 ("halves") + * and a mode 4 ("both") chip. Every check compares what the permuted mock + * returns with an untouched read-only mapping of the dump files, through + * block translations written out here again from the layout definitions + * rather than taken from the mock. + * + * Usage: test_permute DUMPDIR + */ +#include +#include +#include +#include +#include +#include +#include +#include "nand-target.h" +#include "mock_nand.h" + +#define DBANKS 4 +#define DBLOCKS 4096 +#define PPB 128 +#define PS 2048 +#define METASZ 16 +#define DPAGES ((size_t)DBANKS * DBLOCKS * PPB) + +#define CXT_SIZE 0x800 +#define CXT_USED 0x1c +#define CXT_REMAP 0x2c +#define CXT_REMAP_N 820 +#define CXT_RING 0x694 +#define CXT_CKSUM 0x7f8 +#define SPARES 89 +#define FREE 0xfff0 +#define BAD 0xffff + +static int fails; +static const uint8_t *raw_data, *raw_meta; +static int both; /* the layout under test */ +static uint32_t nbanks, nblocks, nunits; + +static void check(const char *what, int ok) +{ + printf("%s %s\n", ok ? "PASS" : "FAIL", what); + if (!ok) + fails++; +} + +static const uint8_t *map_ro(const char *dir, const char *name, size_t want) +{ + char path[512]; + struct stat st; + void *p; + int fd; + + snprintf(path, sizeof(path), "%s/%s", dir, name); + fd = open(path, O_RDONLY); + if (fd < 0 || fstat(fd, &st) || (size_t)st.st_size != want) + { + fprintf(stderr, "%s: missing or wrong size\n", path); + exit(2); + } + p = mmap(NULL, want, PROT_READ, MAP_SHARED | MAP_NORESERVE, fd, 0); + if (p == MAP_FAILED) + { + perror("mmap"); + exit(2); + } + close(fd); + return p; +} + +static uint32_t rng = 0x3a3a1234; +static uint32_t rnd(void) +{ + rng ^= rng << 13; + rng ^= rng >> 17; + rng ^= rng << 5; + return rng; +} + +/* ---- The layouts, from their definitions ---- */ + +/* The formulas' blocks 1 and 2048 (halves) or 4096 (both) trade places, so + * that the dump's context block 1 of the primary bank is new block 1 */ +static uint32_t swap_cxt(uint32_t block) +{ + uint32_t other = both ? 4096 : 2048; + + return block == 1 ? other : block == other ? 1 : block; +} + +/* The formulas alone: where the FTL computes a block, as remap entries + * name replaced blocks */ +static void formula_to_dump(uint32_t bank, uint32_t block, uint32_t *db, + uint32_t *dk) +{ + uint32_t u, v, hi, r; + + if (!both) + { + u = block >= 2048; + v = block - u * 2048; + *db = bank; + *dk = 2 * v + u; + } + else + { + hi = block >= 4096; + r = block - hi * 4096; + v = r / 2; + u = (r & 1) + 2 * hi; + *db = 2 * (u % 2) + bank; + *dk = 2 * v + u / 2; + } +} + +static void formula_from_dump(uint32_t db, uint32_t dk, uint32_t *bank, + uint32_t *block) +{ + uint32_t v = dk / 2, plane = dk % 2, u; + + if (!both) + { + *bank = db; + *block = v + plane * 2048; + } + else + { + u = 2 * plane + db / 2; + *bank = db % 2; + *block = 2 * v + (u & 1) + ((u & 2) ? 4096 : 0); + } +} + +/* Where pages physically are: the formulas with the swap */ +static void to_dump(uint32_t bank, uint32_t block, uint32_t *db, uint32_t *dk) +{ + formula_to_dump(bank, swap_cxt(block), db, dk); +} + +static void from_dump(uint32_t db, uint32_t dk, uint32_t *bank, uint32_t *block) +{ + formula_from_dump(db, dk, bank, block); + *block = swap_cxt(*block); +} + +/* ---- Pages and contexts ---- */ + +static uint16_t get16(const uint8_t *p, size_t off) +{ + return p[off] | p[off + 1] << 8; +} + +static void put16(uint8_t *p, size_t off, uint16_t v) +{ + p[off] = v; + p[off + 1] = v >> 8; +} + +static uint32_t get32(const uint8_t *p, size_t off) +{ + return get16(p, off) | (uint32_t)get16(p, off + 2) << 16; +} + +static void put32(uint8_t *p, size_t off, uint32_t v) +{ + put16(p, off, v); + put16(p, off + 2, v >> 16); +} + +static void cxt_sums(const uint8_t *c, uint32_t *sum, uint32_t *x) +{ + size_t off; + + *sum = *x = 0xAABBCCDD; + for (off = 0; off < CXT_CKSUM; off += 4) + { + *sum += get32(c, off); + *x ^= get32(c, off); + } +} + +static int cxt_ok(const uint8_t *c) +{ + uint32_t sum, x; + + cxt_sums(c, &sum, &x); + return sum == get32(c, CXT_CKSUM) && x == get32(c, CXT_CKSUM + 4); +} + +static void cxt_seal(uint8_t *c) +{ + uint32_t sum, x; + + cxt_sums(c, &sum, &x); + put32(c, CXT_CKSUM, sum); + put32(c, CXT_CKSUM + 4, x); +} + +static int is_cxt(const uint32_t *meta, int rc) +{ + return rc > 0 && rc != NAND_ECC_FAILED + && ((meta[2] >> 8) & 0xff) == 0x80 && (meta[2] & 0xff) == 0; +} + +typedef int (*reader)(uint32_t bank, uint32_t page, uint8_t *buf, + uint32_t *meta); + +/* A page of the dump files, as the NAND driver would read it */ +static int raw_read(uint32_t bank, uint32_t page, uint8_t *buf, uint32_t *meta) +{ + size_t g = (size_t)bank * DBLOCKS * PPB + page; + int32_t rc; + + memcpy(&rc, raw_meta + g * METASZ + 12, 4); + if (rc == NAND_ECC_CLEAN) + { + memset(buf, 0xff, PS); + memset(meta, 0xff, 12); + return rc; + } + memcpy(buf, raw_data + g * PS, PS); + memcpy(meta, raw_meta + g * METASZ, 12); + return rc; +} + +static int mock_read(uint32_t bank, uint32_t page, uint8_t *buf, uint32_t *meta) +{ + return nand_read_page(bank, page, buf, meta); +} + +/* ftl_vfl_read_cxt(): the first good copy of a commit, into buf */ +static int read_cxt(reader rd, uint32_t bank, uint32_t block, uint32_t page, + uint8_t *buf, uint32_t *meta) +{ + uint32_t i; + + for (i = page; i < page + 8 && i < PPB; i++) + if (is_cxt(meta, rd(bank, block * PPB + i, buf, meta)) && cxt_ok(buf)) + return 0; + return -1; +} + +/* The first block from 1 below limit with a context at page 0, or -1 */ +static long scan_cxt(reader rd, uint32_t bank, uint32_t limit, uint8_t *buf) +{ + uint32_t meta[3], block; + + for (block = 1; block < limit; block++) + if (!read_cxt(rd, bank, block, 0, buf, meta)) + return block; + return -1; +} + +/* ftl_vfl_open_bank() on a bank of blocks, with the ring taken from the + * first context below block limit, into out. Returns 0 or -1. */ +static int newest_cxt(reader rd, uint32_t bank, uint32_t limit, + uint32_t blocks, uint8_t *out) +{ + static uint8_t buf[PS]; + uint32_t meta[3], ring[4], i, best = 4, bestusn = 0xffffffff, last; + + if (scan_cxt(rd, bank, limit, buf) < 0) + return -1; + for (i = 0; i < 4; i++) + ring[i] = get16(buf, CXT_RING + 2 * i); + for (i = 0; i < 4; i++) + { + if (ring[i] >= blocks || read_cxt(rd, bank, ring[i], 0, buf, meta)) + continue; + if (meta[0] && meta[0] <= bestusn) + { + bestusn = meta[0]; + best = i; + } + } + if (best == 4) + return -1; + last = 0; + for (i = 8; i < PPB; i += 8) + { + if (read_cxt(rd, bank, ring[best], i, buf, meta)) + break; + last = i; + } + if (read_cxt(rd, bank, ring[best], last, buf, meta)) + return -1; + memcpy(out, buf, CXT_SIZE); + return 0; +} + +static uint8_t dump_newest[DBANKS][CXT_SIZE]; + +/* A remap entry: by the formula, free and bad slots kept */ +static uint16_t map_entry(uint32_t db, uint16_t e) +{ + uint32_t bank, block; + + if (e == FREE || e == BAD) + return e; + formula_from_dump(db, e, &bank, &block); + return block; +} + +/* A ring block: where it now sits */ +static uint16_t map_ring(uint32_t db, uint16_t e) +{ + uint32_t bank, block; + + from_dump(db, e, &bank, &block); + return block; +} + +/* The dump bank a new bank's context header comes from: itself in halves; + * in both, whichever of ce and ce + 2 has the higher usn, as the FTL takes + * its control blocks from the highest usn */ +static uint32_t header_bank(uint32_t bank) +{ + if (!both) + return bank; + return get32(dump_newest[bank + 2], 0) > get32(dump_newest[bank], 0) + ? bank + 2 : bank; +} + +/* What a dump context page should read as: halves rewrites each page */ +static void expect_halves(uint32_t db, const uint8_t *src, uint8_t *out) +{ + uint32_t i; + + memcpy(out, src, CXT_SIZE); + for (i = 0; i < 2 * SPARES; i++) + put16(out, CXT_REMAP + 2 * i, + map_entry(db, get16(src, CXT_REMAP + 2 * i))); + for (i = 0; i < 4; i++) + if (get16(src, CXT_RING + 2 * i) < DBLOCKS) + put16(out, CXT_RING + 2 * i, + map_ring(db, get16(src, CXT_RING + 2 * i))); + cxt_seal(out); +} + +/* ... and both writes one merge of its two banks' newest into all of them */ +static void expect_both(uint32_t ce, uint8_t *out) +{ + uint32_t u, i, db; + + memcpy(out, dump_newest[header_bank(ce)], CXT_SIZE); + memset(out + CXT_REMAP, 0, 2 * CXT_REMAP_N); + for (u = 0; u < 4; u++) + { + db = 2 * (u % 2) + ce; + put16(out, CXT_USED + 2 * u, + get16(dump_newest[db], CXT_USED + 2 * (u / 2))); + for (i = 0; i < SPARES; i++) + put16(out, CXT_REMAP + 2 * (u * SPARES + i), + map_entry(db, get16(dump_newest[db], + CXT_REMAP + 2 * ((u / 2) * SPARES + i)))); + } + for (i = 0; i < 4; i++) + { + uint16_t e = get16(dump_newest[ce], CXT_RING + 2 * i); + put16(out, CXT_RING + 2 * i, e < DBLOCKS ? map_ring(ce, e) : e); + } + cxt_seal(out); +} + +/* ---- The tests ---- */ + +static void test_geometry(void) +{ + const struct nand_geometry *g = nand_get_geometry(); + char what[128]; + + snprintf(what, sizeof(what), "%s: geometry and bank count", + both ? "both" : "halves"); + check(what, g->banks == nbanks && g->blocks == nblocks + && g->pagesperblock == PPB && g->planes == nunits + && g->userblocks == (both ? 7744u : 3872u) && g->vflspares == SPARES + && !g->twoplane && g->pagesize == PS + && g->mode == (both ? 4u : 9u) + && g->layout == (both ? NAND_LAYOUT_BOTH : NAND_LAYOUT_HALVES) + && g->validated && nand_get_bank_count() == nbanks); +} + +/* 2: every dump block is some block of the new layout, exactly once */ +static void test_bijection(void) +{ + static uint8_t hits[DBANKS * DBLOCKS]; + uint32_t bank, block, db, dk, db2, dk2, b2, k2; + unsigned long bad = 0, missed = 0, i; + + memset(hits, 0, sizeof(hits)); + for (bank = 0; bank < nbanks; bank++) + for (block = 0; block < nblocks; block++) + { + to_dump(bank, block, &db, &dk); + mock_layout_to_dump(bank, block, &db2, &dk2); + mock_layout_from_dump(db, dk, &b2, &k2); + if (db >= DBANKS || dk >= DBLOCKS || db != db2 || dk != dk2 + || b2 != bank || k2 != block) + { + bad++; + continue; + } + hits[db * DBLOCKS + dk]++; + } + for (i = 0; i < DBANKS * DBLOCKS; i++) + if (hits[i] != 1) + missed++; + printf(" %lu mistranslated, %lu dump blocks not hit once\n", + bad, missed); + check("translation is a bijection matching the layout definition", + !bad && !missed); +} + +/* 1: random pages read the same as the dump's; context pages excepted */ +static void test_random_reads(void) +{ + static uint8_t buf[PS], want[PS]; + uint32_t meta[3], wmeta[3], db, dk, bank, page; + struct nand_read run[8]; + static uint8_t runbuf[8][PS]; + uint32_t runmeta[8][3]; + unsigned long n = 0, bad = 0, written = 0, cxts = 0; + int i, j, rc, wrc; + + for (i = 0; i < 4000; i++) + { + bank = rnd() % nbanks; + page = rnd() % (nblocks * PPB); + to_dump(bank, page / PPB, &db, &dk); + wrc = raw_read(db, dk * PPB + page % PPB, want, wmeta); + if (is_cxt(wmeta, wrc)) + { + cxts++; + continue; + } + rc = nand_read_page(bank, page, buf, meta); + n++; + written += wrc != NAND_ECC_CLEAN; + if (rc != wrc || memcmp(meta, wmeta, sizeof(meta)) + || memcmp(buf, want, PS)) + bad++; + } + /* The same through read runs, pages of different banks mixed */ + for (i = 0; i < 125; i++) + { + for (j = 0; j < 8; j++) + { + run[j].bank = rnd() % nbanks; + run[j].page = rnd() % (nblocks * PPB); + run[j].buf = runbuf[j]; + run[j].meta = runmeta[j]; + } + if (nand_read_pages(run, 8)) + { + bad++; + continue; + } + for (j = 0; j < 8; j++) + { + to_dump(run[j].bank, run[j].page / PPB, &db, &dk); + wrc = raw_read(db, dk * PPB + run[j].page % PPB, want, wmeta); + if (is_cxt(wmeta, wrc)) + { + cxts++; + continue; + } + n++; + written += wrc != NAND_ECC_CLEAN; + if (run[j].ecc != wrc || memcmp(runmeta[j], wmeta, sizeof(wmeta)) + || memcmp(runbuf[j], want, PS)) + bad++; + } + } + printf(" %lu pages compared (%lu written), %lu context pages " + "skipped, %lu differ\n", n, written, cxts, bad); + check("random pages read as the dump's", !bad && written > 100); +} + +/* 1, context pages: every one in the dump, at its new place */ +static void test_cxt_pages(void) +{ + static uint8_t buf[PS], want[PS], expect[CXT_SIZE]; + uint32_t meta[3], wmeta[3], db, dk, bank, block; + unsigned long n = 0, bad = 0, invalid = 0; + size_t g; + int rc, wrc; + + for (g = 0; g < DPAGES; g++) + { + uint32_t w2; + int32_t r; + + memcpy(&w2, raw_meta + g * METASZ + 8, 4); + memcpy(&r, raw_meta + g * METASZ + 12, 4); + if (((w2 >> 8) & 0xff) != 0x80 || (w2 & 0xff) || r == NAND_ECC_CLEAN) + continue; + db = g / ((size_t)DBLOCKS * PPB); + dk = (g / PPB) % DBLOCKS; + wrc = raw_read(db, dk * PPB + g % PPB, want, wmeta); + if (!cxt_ok(want)) + { + invalid++; + continue; + } + from_dump(db, dk, &bank, &block); + rc = nand_read_page(bank, block * PPB + g % PPB, buf, meta); + if (both) + expect_both(bank, expect); + else + expect_halves(db, want, expect); + n++; + if (rc != wrc || memcmp(meta, wmeta, sizeof(meta)) || !cxt_ok(buf) + || memcmp(buf, expect, CXT_SIZE) + || memcmp(buf + CXT_SIZE, want + CXT_SIZE, PS - CXT_SIZE)) + bad++; + } + printf(" %lu context pages, %lu differ from the expected rewrite, " + "%lu with bad checksums left alone\n", n, bad, invalid); + check("context pages read as rewritten, spare and tail unchanged", + n && !bad); +} + +/* 3: each bank's context can be found, and its ring holds contexts */ +static void test_scan(void) +{ + static uint8_t buf[PS], cxt[PS], rawbuf[PS]; + uint32_t meta[3], bank, i, ring, db, dk; + long at; + char what[160]; + int ok, newok, rawok, anyok; + + for (bank = 0; bank < nbanks; bank++) + { + at = scan_cxt(mock_read, bank, 200, cxt); + snprintf(what, sizeof(what), + "bank %u: a context in physical blocks 1..199", bank); + if (at < 0) + { + at = scan_cxt(mock_read, bank, nblocks, cxt); + if (at >= 0) + { + to_dump(bank, at, &db, &dk); + printf(" bank %u: first context at block %ld " + "(dump bank %u block %u)\n", bank, at, db, dk); + } + } + else + printf(" bank %u: first context at block %ld\n", bank, at); + check(what, at >= 0 && at < 200); + if (at < 0) + continue; + + /* The dump's rings list blocks that were never written, so a ring + * block must hold a context exactly when the block its pages come + * from does in the dump, and one must */ + ok = 1; + anyok = 0; + printf(" ring:"); + for (i = 0; i < 4; i++) + { + ring = get16(cxt, CXT_RING + 2 * i); + if (ring >= nblocks) + { + ok = 0; + continue; + } + to_dump(bank, ring, &db, &dk); + newok = !read_cxt(mock_read, bank, ring, 0, buf, meta); + rawok = !read_cxt(raw_read, db, dk, 0, rawbuf, meta); + printf(" %u%s", ring, newok ? "(cxt)" : ""); + ok &= newok == rawok; + anyok |= newok; + } + printf("\n"); + snprintf(what, sizeof(what), + "bank %u: ring blocks hold contexts at page 0 as in the dump", + bank); + check(what, ok && anyok); + } +} + +/* 4: the newest context's tables name the same blocks as the dump's */ +static void test_remap(void) +{ + static uint8_t cxt[CXT_SIZE]; + uint32_t bank, u, i, sb, splane, db, dk, n, o, hb; + char what[128]; + unsigned long entries, bad; + int ok; + + for (bank = 0; bank < nbanks; bank++) + { + snprintf(what, sizeof(what), + "bank %u: newest context's remap tables and ring", bank); + if (newest_cxt(mock_read, bank, nblocks, nblocks, cxt)) + { + check(what, 0); + continue; + } + entries = bad = 0; + hb = header_bank(bank); + ok = cxt_ok(cxt) + && !memcmp(cxt, dump_newest[hb], CXT_USED) + && !memcmp(cxt + CXT_RING + 8, dump_newest[hb] + CXT_RING + 8, + CXT_CKSUM - CXT_RING - 8); + for (u = 0; u < nunits; u++) + { + sb = both ? 2 * (u % 2) + bank : bank; + splane = both ? u / 2 : u; + if (get16(cxt, CXT_USED + 2 * u) + != get16(dump_newest[sb], CXT_USED + 2 * splane)) + ok = 0; + for (i = 0; i < SPARES; i++) + { + o = get16(dump_newest[sb], CXT_REMAP + 2 * (splane * SPARES + i)); + n = get16(cxt, CXT_REMAP + 2 * (u * SPARES + i)); + if (o == FREE || o == BAD) + { + bad += n != o; + continue; + } + entries++; + formula_to_dump(bank, n, &db, &dk); + bad += n >= nblocks || db != sb || dk != o; + } + } + for (i = nunits * SPARES; i < CXT_REMAP_N; i++) + bad += get16(cxt, CXT_REMAP + 2 * i) != 0; + for (i = 0; i < 4; i++) + { + o = get16(dump_newest[bank], CXT_RING + 2 * i); + n = get16(cxt, CXT_RING + 2 * i); + to_dump(bank, n, &db, &dk); + if (o < DBLOCKS ? (db != bank || dk != o) : n != o) + ok = 0; + } + printf(" bank %u: usn %u, control blocks %u %u %u (header from " + "dump bank %u), %lu remapped entries, %lu wrong\n", + bank, get32(cxt, 0), get16(cxt, 4), get16(cxt, 6), + get16(cxt, 8), hb, entries, bad); + check(what, ok && !bad); + } +} + +/* 5: erase, program, read a translated block; its neighbours stay */ +static int block_is_dump(uint32_t bank, uint32_t block) +{ + static uint8_t buf[PS], want[PS]; + uint32_t meta[3], wmeta[3], db, dk, p; + int rc, wrc; + + to_dump(bank, block, &db, &dk); + for (p = 0; p < PPB; p++) + { + wrc = raw_read(db, dk * PPB + p, want, wmeta); + rc = nand_read_page(bank, block * PPB + p, buf, meta); + if (rc != wrc || memcmp(meta, wmeta, sizeof(meta))) + return 0; + if (!is_cxt(wmeta, wrc) && memcmp(buf, want, PS)) + return 0; + } + return 1; +} + +static void test_erase_program(void) +{ + static uint8_t buf[PS], pat[PS]; + uint32_t meta[3], wmeta[3], bank, block, db, dk, p, i, nb[4], nk[4]; + uint32_t written = 0; + int ok, rc; + size_t g; + + /* A written block in the upper half of the new numbering */ + bank = 1; + block = both ? 4101 : 2100; + for (; block + 2 < nblocks; block += 2) + { + to_dump(bank, block, &db, &dk); + written = 0; + for (p = 0; p < PPB; p++) + written += raw_read(db, dk * PPB + p, buf, meta) != NAND_ECC_CLEAN; + if (written == PPB) + break; + } + /* Neighbours in the new numbering, and in the dump's */ + nb[0] = nb[1] = bank; + nk[0] = block - 1; + nk[1] = block + 1; + from_dump(db, dk - 1, &nb[2], &nk[2]); + from_dump(db, dk + 1, &nb[3], &nk[3]); + printf(" bank %u block %u (dump bank %u block %u), neighbours", + bank, block, db, dk); + for (i = 0; i < 4; i++) + printf(" %u:%u", nb[i], nk[i]); + printf("\n"); + + ok = nand_erase_block(bank, block) == 0; + for (p = 0; p < PPB && ok; p++) + ok = nand_read_page(bank, block * PPB + p, buf, meta) == NAND_ECC_CLEAN; + check("erase of a translated block reads back erased", ok); + + ok = 1; + for (p = 0; p < 4; p++) + { + for (i = 0; i < PS; i++) + pat[i] = i * 7 + p; + wmeta[0] = 0x100 + p; + wmeta[1] = 0xffffffff; + wmeta[2] = 0xffff4000; + ok &= nand_write_page(bank, block * PPB + p, pat, wmeta) == 0; + rc = nand_read_page(bank, block * PPB + p, buf, meta); + ok &= rc == NAND_ECC_CORRECTED && !memcmp(buf, pat, PS) + && !memcmp(meta, wmeta, sizeof(meta)); + /* ... and it landed in the dump block the layout says */ + g = ((size_t)db * DBLOCKS + dk) * PPB + p; + ok &= !memcmp(mock_data + g * PS, pat, PS); + } + ok &= !mock_reprogram && !mock_nonsequential; + check("program of a translated block reads back, in its dump block", ok); + + ok = 1; + for (i = 0; i < 4; i++) + ok &= block_is_dump(nb[i], nk[i]); + check("neighbouring blocks, new and dump numbering, undisturbed", ok); + + /* Bad blocks are named in the new numbering */ + mock_clear_bad(); + mock_set_bad(bank, block, MOCK_FAIL_WRITE | MOCK_FAIL_ERASE); + ok = nand_erase_block(bank, block) == NAND_OP_FAILED + && nand_write_page(bank, block * PPB + 4, pat, wmeta) + == NAND_OP_FAILED; + ok &= nand_read_page(bank, block * PPB + 3, buf, meta) + == NAND_ECC_CORRECTED; + mock_clear_bad(); + mock_set_bad(db, dk, MOCK_FAIL_WRITE); + ok &= nand_write_page(bank, block * PPB + 4, pat, wmeta) == 0; + mock_clear_bad(); + check("bad-block injection uses the new numbering", ok); + + /* A torn program lands on the translated page */ + mock_crash_torn = 1; + mock_crash_after = 1; + if (!setjmp(mock_crash_jmp)) + { + nand_write_page(bank, block * PPB + 5, pat, wmeta); + ok = 0; + } + else + ok = nand_read_page(bank, block * PPB + 5, buf, meta) + == NAND_ECC_FAILED + && nand_read_page(bank, block * PPB + 4, buf, meta) + == NAND_ECC_CORRECTED; + mock_crash_torn = 0; + mock_crash_after = 0; + for (i = 0; i < 4; i++) + ok &= block_is_dump(nb[i], nk[i]); + check("crash injection cuts the translated page only", ok); +} + +static void run(const char *dir, const char *layout) +{ + printf("== %s\n", layout); + both = !strcmp(layout, "both"); + nbanks = both ? 2 : 4; + nblocks = both ? 8192 : 4096; + nunits = both ? 4 : 2; + mock_open_layout(dir, layout); + test_geometry(); + test_bijection(); + test_random_reads(); + test_cxt_pages(); + test_scan(); + test_remap(); + test_erase_program(); +} + +int main(int argc, char **argv) +{ + uint32_t b; + + if (argc != 2) + { + fprintf(stderr, "usage: %s DUMPDIR\n", argv[0]); + return 2; + } + raw_data = map_ro(argv[1], "nand_data_4banks.bin", DPAGES * PS); + raw_meta = map_ro(argv[1], "meta_all.bin", DPAGES * METASZ); + for (b = 0; b < DBANKS; b++) + { + if (newest_cxt(raw_read, b, 200, DBLOCKS, dump_newest[b])) + { + fprintf(stderr, "dump bank %u: no VFL context\n", b); + return 2; + } + printf("dump bank %u: newest context usn %u\n", b, + get32(dump_newest[b], 0)); + } + + run(argv[1], "halves"); + run(argv[1], "both"); + + printf("%s\n", fails ? "SOME FAILED" : "ALL PASS"); + return fails ? 1 : 0; +} diff --git a/utils/ipodnano3g/ftltest/test_restore.c b/utils/ipodnano3g/ftltest/test_restore.c new file mode 100644 index 0000000000..3da083d412 --- /dev/null +++ b/utils/ipodnano3g/ftltest/test_restore.c @@ -0,0 +1,149 @@ +/* Closed data blocks the committed map does not know about. + * + * Apple's disk mode writes whole logical blocks - closed superblocks, last + * page type 0x41 - without committing its block map, so after a large copy + * the committed map is older than dozens of them. Apple's own _FTLRestore + * puts every closed block into the map, the highest usn winning, and treats + * only unclosed ones as logs. Found on the device: after a 77 MB copy in + * Apple's disk mode, Rockbox's mount failed with "more than 17 log blocks". + * + * Simulated here by writing whole blocks through our own FTL, with its + * commits, and then rolling the three control superblocks back to their + * earlier contents: the committed map and pool are old again, and the + * blocks written since are closed but unmapped. + */ +#include +#include +#include +#include "nand-target.h" +#include "ftl-target.h" +#include "mock_nand.h" + +#define SBPAGES 1024 +#define PAGES (4096UL * 128) /* per bank, as mock_nand.c lays them out */ +#define METASZ 16 +#define BASE 1500 +#define NBLK 25 + +static int fails; + +static void check(const char *what, int ok) +{ + printf("%s %s\n", ok ? "PASS" : "FAIL", what); + if (!ok) + fails++; +} + +static void fill(uint8_t *buf, uint32_t lblock) +{ + uint32_t i; + + for (i = 0; i < SBPAGES * NAND_PAGE_SIZE; i++) + buf[i] = (uint8_t)(i * 7 + lblock * 13 + 1); +} + +/* Copy every physical page of the control superblocks out or back in */ +static void ctrl_pages(uint16_t *ctrl, uint8_t *data, uint8_t *meta, int save) +{ + uint32_t i, v, bank, page; + size_t g, k = 0; + + for (i = 0; i < 3; i++) + for (v = 0; v < SBPAGES; v++, k++) + { + if (ftl_sb_locate(ctrl[i], v, &bank, &page)) + { + puts("locate failed"); + exit(1); + } + g = bank * PAGES + page; + if (save) + { + memcpy(data + k * NAND_PAGE_SIZE, + mock_data + g * NAND_PAGE_SIZE, NAND_PAGE_SIZE); + memcpy(meta + k * METASZ, mock_meta + g * METASZ, METASZ); + } + else + { + memcpy(mock_data + g * NAND_PAGE_SIZE, + data + k * NAND_PAGE_SIZE, NAND_PAGE_SIZE); + memcpy(mock_meta + g * METASZ, meta + k * METASZ, METASZ); + } + } +} + +int main(int argc, char **argv) +{ + static uint8_t blk[SBPAGES * NAND_PAGE_SIZE], buf[SBPAGES * NAND_PAGE_SIZE]; + static uint8_t cdata[3 * SBPAGES * NAND_PAGE_SIZE], cmeta[3 * SBPAGES * METASZ]; + uint16_t ctrl[3]; + uint32_t k, bad; + int ok = 1, rc; + + mock_open(argc > 1 ? argv[1] : "../n3g-dump"); + if (ftl_init()) { puts("mount failed"); return 1; } + check("settles", ftl_hook_settle() == 0); + /* A write before anything else marks the context dirty (0x4f), as + * Apple's disk mode does; that mark is part of what gets rolled back */ + { + static uint8_t s0[NAND_PAGE_SIZE]; + check("a write marks the context dirty", + ftl_read(0, 1, s0) == 0 && ftl_write(0, 1, s0) == 0); + } + + ftl_hook_ctrl(ctrl); + ctrl_pages(ctrl, cdata, cmeta, 1); + printf(" control superblocks %u %u %u saved\n", ctrl[0], ctrl[1], ctrl[2]); + + for (k = 0; k < NBLK && ok; k++) + { + fill(blk, BASE + k); + ok = ftl_write((BASE + k) * SBPAGES, SBPAGES, blk) == 0; + } + check("25 whole-block writes", ok); + check("committed", ftl_sync() == 0); + + ctrl_pages(ctrl, cdata, cmeta, 0); + puts(" control superblocks rolled back: the committed map predates them"); + + rc = ftl_init(); + printf(" ftl_init() = %d\n", rc); + check("mounts with 25 closed blocks the committed map lacks", rc == 0); + if (rc) + { + printf("SOME CHECKS FAILED\n"); + return 1; + } + + bad = 0; + for (k = 0; k < NBLK; k++) + { + fill(blk, BASE + k); + if (ftl_read((BASE + k) * SBPAGES, SBPAGES, buf) || memcmp(buf, blk, sizeof(blk))) + bad++; + } + printf(" %u of %u blocks read wrong\n", bad, NBLK); + check("every closed block is read", bad == 0); + check("writable", ftl_hook_writeable()); + + /* A write, a commit and a clean remount must keep all of it */ + memset(buf, 0x5a, NAND_PAGE_SIZE); + check("a small write after the mount", ftl_write(BASE * SBPAGES + 3, 1, buf) == 0); + check("sync", ftl_sync() == 0); + check("clean remount", ftl_init() == 0); + bad = 0; + for (k = 0; k < NBLK; k++) + { + fill(blk, BASE + k); + if (k == 0) + memset(blk + 3 * NAND_PAGE_SIZE, 0x5a, NAND_PAGE_SIZE); + if (ftl_read((BASE + k) * SBPAGES, SBPAGES, buf) || memcmp(buf, blk, sizeof(blk))) + bad++; + } + printf(" %u of %u blocks read wrong after the commit\n", bad, NBLK); + check("all of it survives a commit and remount", bad == 0); + check("NAND rules kept", mock_reprogram == 0 && mock_nonsequential == 0); + + printf(fails ? "SOME CHECKS FAILED\n" : "ALL PASS\n"); + return fails ? 1 : 0; +} diff --git a/utils/ipodnano3g/ftltest/test_wear.c b/utils/ipodnano3g/ftltest/test_wear.c new file mode 100644 index 0000000000..1e908eeee6 --- /dev/null +++ b/utils/ipodnano3g/ftltest/test_wear.c @@ -0,0 +1,157 @@ +/* Wear behaviour of the free pool: allocation must take the least-worn + * block, as Apple's _GetFreeVb does, and repeated writes must spread over + * the pool rather than grinding one block down. */ +#include +#include +#include +#include "nand-target.h" +#include "ftl-target.h" +#include "mock_nand.h" + +#define SBPAGES 1024 +#define POOL 20 + +static int fails; + +static void check(const char *what, int ok) +{ + printf("%s %s\n", ok ? "PASS" : "FAIL", what); + if (!ok) + fails++; +} + +int main(int argc, char **argv) +{ + static uint8_t buf[NAND_PAGE_SIZE]; + uint16_t sb[POOL], ec[POOL]; + uint32_t n, i, bank, page, got, lowest, lowec, spread_before, spread_after; + uint32_t mn, mx, round, poolmax_initial; + + mock_open(argc > 1 ? argv[1] : "../n3g-dump"); + if (ftl_init()) { puts("mount failed"); return 1; } + check("mounts writeable", ftl_hook_writeable()); + /* settle: fold inherited logs so the pool is whole */ + check("initial sync", ftl_hook_settle() == 0); + + n = ftl_pool_snapshot(sb, ec, POOL); + check("the pool is full after a sync", n == POOL); + if (n != POOL) + return 1; + mn = 0xffff; mx = 0; + lowest = sb[0]; lowec = ec[0]; + for (i = 0; i < n; i++) + { + if (ec[i] < mn) mn = ec[i]; + if (ec[i] > mx) mx = ec[i]; + if (ec[i] < lowec) { lowec = ec[i]; lowest = sb[i]; } + } + spread_before = mx - mn; + poolmax_initial = mx; + printf(" pool erase counts: min %u max %u spread %u; least worn is sb %u\n", + mn, mx, spread_before, lowest); + check("the pool is not uniformly worn (so the choice is observable)", + spread_before > 0); + + /* One write: the block it lands in must be the least worn in the pool */ + memset(buf, 0xa5, sizeof(buf)); + check("write", ftl_write(700 * SBPAGES + 1, 1, buf) == 0); + check("locate", ftl_locate(700 * SBPAGES + 1, &bank, &page) == 0); + /* Is that physical page inside the least-worn superblock? A small write + * now lands in a log block, at whatever offset the append cursor was + * on, so look for the page anywhere in the block rather than at its + * index within the logical block. */ + got = 0xffffffff; + for (i = 0; i < SBPAGES; i++) + { + uint32_t b2, p2; + + if (!ftl_sb_locate(lowest, i, &b2, &p2) && b2 == bank && p2 == page) + { + got = lowest; + break; + } + } + printf(" the write landed in sb %u (least worn was %u, ec %u)\n", + got == 0xffffffff ? 0xffffffff : lowest, lowest, lowec); + check("allocation took the least-worn block", got == lowest); + + /* Hammer one logical block and watch the pool even out rather than + * one block taking every cycle */ + for (round = 0; round < 60; round++) + { + memset(buf, (uint8_t)round, sizeof(buf)); + if (ftl_write(700 * SBPAGES + 1, 1, buf) || ftl_sync()) + { + check("60 write+sync rounds", 0); + return 1; + } + } + check("60 write+sync rounds", ftl_init() == 0); + n = ftl_pool_snapshot(sb, ec, POOL); + mn = 0xffff; mx = 0; + for (i = 0; i < n; i++) + { + if (ec[i] < mn) mn = ec[i]; + if (ec[i] > mx) mx = ec[i]; + } + spread_after = mx - mn; + printf(" after 60 rounds: min %u max %u spread %u (was %u)\n", + mn, mx, spread_after, spread_before); + /* Apple's cadence (one move per 20 merges' budget) lets cold blocks + * into the pool, which widens the pool's spread; reported, not judged */ + printf(" pool spread %s\n", spread_after <= spread_before + ? "did not grow" : "grew as cold blocks joined the pool"); + check("the data still reads back", + ftl_read(700 * SBPAGES + 1, 1, buf) == 0 && buf[0] == 59); + + /* Static wear levelling. The signal is not the whole-device spread - + * 1230 of the 1959 superblocks sit near the minimum, so one relocation + * per commit cannot move it in any reasonable number of rounds, and the + * device maximum never falls at all once that block holds cold data. + * The signal is the *pool*: worn blocks should be retired into cold-data + * duty and cold blocks recruited in their place, until the spread is + * under the threshold - at which point levelling must stop, so the cost + * stays bounded. */ + { + uint32_t gmin, gmax, r, poolmax_before, poolmax_after; + uint16_t s2[POOL], e2[POOL]; + + ftl_global_spread(&gmin, &gmax); + printf(" whole-device erase counts: min %u max %u\n", gmin, gmax); + poolmax_before = poolmax_initial; /* before any levelling ran */ + + for (r = 0; r < 120; r++) + { + memset(buf, (uint8_t)r, sizeof(buf)); + if (ftl_write(300 * SBPAGES + 5, 1, buf) || ftl_sync()) + { + check("120 more write+sync rounds", 0); + return 1; + } + } + check("120 more write+sync rounds", ftl_init() == 0); + + n = ftl_pool_snapshot(s2, e2, POOL); + mn = 0xffff; poolmax_after = 0; + for (i = 0; i < n; i++) + { + if (e2[i] < mn) mn = e2[i]; + if (e2[i] > poolmax_after) poolmax_after = e2[i]; + } + printf(" pool after levelling: min %u max %u (started at %u, " + "device max %u)\n", mn, poolmax_after, poolmax_before, gmax); + check("the pool is still whole", n == POOL); + check("worn blocks were retired out of the pool", + poolmax_after < poolmax_before); + check("the pool no longer holds the device's most worn block", + poolmax_after < gmax); + printf(" pool spread after levelling: %u\n", poolmax_after - mn); + check("the data still reads back after levelling", + ftl_read(300 * SBPAGES + 5, 1, buf) == 0 && buf[0] == 119); + check("and the earlier block too", + ftl_read(700 * SBPAGES + 1, 1, buf) == 0 && buf[0] == 59); + } + + printf(fails ? "SOME CHECKS FAILED\n" : "ALL PASS\n"); + return fails ? 1 : 0; +} diff --git a/utils/ipodnano3g/ftltest/test_write.c b/utils/ipodnano3g/ftltest/test_write.c new file mode 100644 index 0000000000..cfc182dad8 --- /dev/null +++ b/utils/ipodnano3g/ftltest/test_write.c @@ -0,0 +1,492 @@ +/* Write-path test for ftl-nano3g.c: write, sync, remount, read back - all + * against a copy-on-write mapping of the 4GB dump, never a device. + * + * Beyond "does the data come back", this checks two things that matter for + * a real iPod: that we never break a NAND rule (no reprogram, no page + * skipped inside a block), and that the image we leave behind still + * satisfies the invariants Apple's own _LoadFTLCxt and _FTLRestore check - + * so its firmware can still mount what we wrote. */ +#include +#include +#include +#include "nand-target.h" +#include "ftl-target.h" +#include "mock_nand.h" + +#define BANKS 4 +#define BLOCKS 4096 +#define PPB 128 +#define PLANES 2 +#define SBPAGES (PPB * PLANES * BANKS) /* 1024 */ +#define NSB ((BLOCKS - PLANES * 89) / PLANES) /* 1959 superblocks */ +#define NPAGES ((size_t)BANKS * BLOCKS * PPB) +#define POOL 20 + +static int fails; + +/* src for the scattered-write test */ +struct scat_ctx { uint32_t lpn[2]; uint8_t *p[2]; }; + +static const void *scat_src(uint32_t lpn, void *ctx) +{ + struct scat_ctx *s = ctx; + int i; + + for (i = 0; i < 2; i++) + if (s->lpn[i] == lpn) + return s->p[i]; + return NULL; +} + +static void check(const char *what, int ok) +{ + printf("%s %s\n", ok ? "PASS" : "FAIL", what); + if (!ok) + fails++; +} + +static uint32_t meta_word(size_t g, int w) +{ + uint32_t v; + memcpy(&v, mock_meta + g * 16 + 4 * w, 4); + return v; +} + +static int meta_type_g(size_t g) +{ + return mock_meta[g * 16 + 9]; +} + +/* (superblock, position within it) -> physical page, through the FTL's own + * mapping so that VFL block remapping is accounted for */ +static size_t g_of(uint32_t sb, uint32_t v) +{ + uint32_t bank, page; + + if (ftl_sb_locate(sb, v, &bank, &page)) + return (size_t)-1; + return (size_t)bank * BLOCKS * PPB + page; +} + +/* 0xff - "erased" - for a page that cannot be located, so a failed mount + * reports failed checks instead of reading out of bounds */ +static int type_at(uint32_t sb, uint32_t v) +{ + size_t g = g_of(sb, v); + + return g == (size_t)-1 ? 0xff : meta_type_g(g); +} + +/* Would Apple's firmware mount this image? Re-derives everything from the + * spare metadata, using none of the FTL's own state. */ +static void check_apple_invariants(uint32_t usersb) +{ + static uint8_t state[NSB]; + uint32_t sb, v, ctrl[3], nctrl = 0, ndata = 0, nfree = 0, nlog = 0; + uint32_t best = NSB, bestv = 0, bestusn = 0xffffffff; + int last_is_cxt; + + for (sb = 0; sb < NSB; sb++) + { + int tlast = type_at(sb, SBPAGES - 1); + int tfirst = type_at(sb, 0); + + if (tlast == 0x41) + state[sb] = 0x41, ndata++; + else if (tlast >= 0x43 && tlast <= 0x4f) + { + state[sb] = 0x42; + if (nctrl < 3) + ctrl[nctrl] = sb; + nctrl++; + } + else if (tlast == 0xff && tfirst == 0xff) + state[sb] = 0x48, nfree++; + else if (tlast == 0xff && tfirst == 0x40) + state[sb] = 0x40, nlog++; + else if (tlast == 0xff && tfirst >= 0x43 && tfirst <= 0x4f) + { + state[sb] = 0x42; + if (nctrl < 3) + ctrl[nctrl] = sb; + nctrl++; + } + else + state[sb] = 0; + } + + check("every superblock classifies as data, control, log or free", + ndata + nctrl + nfree + nlog == NSB); + check("one data superblock per logical block", ndata == usersb); + check("exactly three control superblocks", nctrl == 3); + check("free blocks + logs == 20 (_FTLRestore's invariant)", + nfree + nlog == POOL); + + /* _LoadFTLCxt: of the three control blocks, the one whose page 0 has + * the lowest usn is chosen; scanning it backward past erased pages, + * the first readable page must be the context (0x43). */ + for (v = 0; v < nctrl && v < 3; v++) + { + size_t cg = g_of(ctrl[v], 0); + uint32_t usn = cg == (size_t)-1 ? 0xffffffff : meta_word(cg, 0); + int t = type_at(ctrl[v], 0); + if (t >= 0x43 && t <= 0x4f && usn <= bestusn) + { + bestusn = usn; + best = ctrl[v]; + } + } + check("a control block is selectable by page 0", best != NSB); + if (best == NSB) + return; + last_is_cxt = 0; + for (v = SBPAGES; v-- > 0; ) + { + int t = type_at(best, v); + if (t == 0xff) + continue; + last_is_cxt = (t == 0x43); + bestv = v; + break; + } + check("the newest control block's last written page is the context", + last_is_cxt); + check("the context is not the block's very last page (room to grow)", + bestv < SBPAGES - 1); + (void)state; +} + +/* What Apple's _LoadFTLCxt finds: in the control block whose page 0 has the + * lowest usn, the last written page. A context (0x43) there is trusted; a + * 0x4f mark after it makes Apple run _FTLRestore instead. Returns the page + * type and sets *usn to that page's usn. */ +static int apple_ctrl_tail(uint32_t *usn) +{ + uint32_t sb, v, best = NSB, bestusn = 0xffffffff; + + for (sb = 0; sb < NSB; sb++) + { + int t = type_at(sb, 0); + size_t cg = g_of(sb, 0); + + if (t < 0x43 || t > 0x4f || cg == (size_t)-1) + continue; + if (meta_word(cg, 0) <= bestusn) + { + bestusn = meta_word(cg, 0); + best = sb; + } + } + *usn = 0; + if (best == NSB) + return -1; + for (v = SBPAGES; v-- > 0; ) + { + int t = type_at(best, v); + + if (t == 0xff) + continue; + *usn = meta_word(g_of(best, v), 0); + return t; + } + return -1; +} + +/* src for the dirty-mark test: page idx of every block in [first, first+n) */ +struct every_ctx { uint32_t first, nblk, idx; uint8_t *page; }; + +static const void *every_src(uint32_t lpn, void *ctx) +{ + struct every_ctx *e = ctx; + uint32_t b = lpn / SBPAGES; + + if (lpn % SBPAGES != e->idx || b < e->first || b >= e->first + e->nblk) + return NULL; + return e->page; +} + +int main(int argc, char **argv) +{ + const char *dir = argc > 1 ? argv[1] : "../n3g-dump"; + static uint8_t pattern[8 * NAND_PAGE_SIZE]; + static uint8_t saved[8 * NAND_PAGE_SIZE]; + static uint8_t before[8 * NAND_PAGE_SIZE]; + static uint8_t buf[8 * NAND_PAGE_SIZE]; + uint32_t nsec, usersb, base, n = 6, i; + unsigned long sample_bad = 0; + uint32_t *sample_lpn; + uint8_t *sample_first; + int rc; + + mock_open(dir); + rc = ftl_init(); + check("mounts", rc == 0); + if (rc) + return 1; + check("write state loaded", ftl_hook_writeable()); + nsec = ftl_num_sectors(); + usersb = nsec / SBPAGES; + check("capacity is a whole number of superblocks", nsec % SBPAGES == 0); + + /* A sample of the rest of the medium, to prove writing moved nothing + * it should not have */ + sample_lpn = malloc(2048 * sizeof(uint32_t)); + sample_first = malloc(2048); + for (i = 0; i < 2048; i++) + { + sample_lpn[i] = (uint32_t)(((uint64_t)i * 967) % nsec); + if (ftl_read(sample_lpn[i], 1, buf)) + sample_bad++; + sample_first[i] = buf[0]; + } + check("sampled the medium before writing", sample_bad == 0); + + /* Write across a superblock boundary */ + base = 5 * SBPAGES - 3; + check("saved the region being overwritten", ftl_read(base, n, saved) == 0); + check("saved the region just before it", + ftl_read(base - 4, 4, before) == 0); + for (i = 0; i < n * NAND_PAGE_SIZE; i++) + pattern[i] = (uint8_t)(i * 31 + 7); + + mock_reset_stats(); + check("write returns 0", ftl_write(base, n, pattern) == 0); + check("reads back as written before any sync", + ftl_read(base, n, buf) == 0 + && !memcmp(buf, pattern, n * NAND_PAGE_SIZE)); + check("the sectors just before are untouched", + ftl_read(base - 4, 4, buf) == 0 && !memcmp(buf, before, 4 * NAND_PAGE_SIZE)); + check("sync returns 0", ftl_sync() == 0); + check("no page was reprogrammed", mock_reprogram == 0); + check("no page was skipped within a block", mock_nonsequential == 0); + printf(" %lu reads, %lu writes, %lu erases for 6 sectors + sync\n", + mock_reads, mock_writes, mock_erases); + + /* Remount from what was actually written */ + rc = ftl_init(); + check("remounts after the commit", rc == 0); + check("still writeable after remount", ftl_hook_writeable()); + check("capacity unchanged", ftl_num_sectors() == nsec); + check("the written sectors survive the remount", + ftl_read(base, n, buf) == 0 + && !memcmp(buf, pattern, n * NAND_PAGE_SIZE)); + check("the sectors just before survive too", + ftl_read(base - 4, 4, buf) == 0 + && !memcmp(buf, before, 4 * NAND_PAGE_SIZE)); + + /* Overwrite the same region again, to exercise the committed path */ + mock_reset_stats(); + for (i = 0; i < n * NAND_PAGE_SIZE; i++) + pattern[i] = (uint8_t)(i * 17 + 3); + check("second write returns 0", ftl_write(base, n, pattern) == 0); + check("second sync returns 0", ftl_sync() == 0); + check("second write raises no NAND violations", + mock_reprogram == 0 && mock_nonsequential == 0); + rc = ftl_init(); + check("remounts again", rc == 0); + check("the second write survives", + ftl_read(base, n, buf) == 0 + && !memcmp(buf, pattern, n * NAND_PAGE_SIZE)); + + /* Restoring the original content must give back exactly what was there */ + check("restoring the original content works", + ftl_write(base, n, saved) == 0 && ftl_sync() == 0); + check("the region reads as it originally did", + ftl_read(base, n, buf) == 0 && !memcmp(buf, saved, n * NAND_PAGE_SIZE)); + + /* Nothing else moved */ + sample_bad = 0; + for (i = 0; i < 2048; i++) + { + if (sample_lpn[i] >= base && sample_lpn[i] < base + n) + continue; + if (ftl_read(sample_lpn[i], 1, buf) || buf[0] != sample_first[i]) + sample_bad++; + } + check("the rest of the medium is unchanged", sample_bad == 0); + + /* A write spanning several superblocks */ + { + static uint8_t big[48 * NAND_PAGE_SIZE]; + static uint8_t bigback[48 * NAND_PAGE_SIZE]; + uint32_t bbase = 40 * SBPAGES - 20, bn = 48; /* spans 3 superblocks */ + + for (i = 0; i < bn * NAND_PAGE_SIZE; i++) + big[i] = (uint8_t)(i * 5 + 11); + mock_reset_stats(); + check("a write spanning three superblocks returns 0", + ftl_write(bbase, bn, big) == 0); + check("it raises no NAND violations", + mock_reprogram == 0 && mock_nonsequential == 0); + check("sync after it returns 0", ftl_sync() == 0); + check("it survives a remount", + ftl_init() == 0 && ftl_read(bbase, bn, bigback) == 0 + && !memcmp(bigback, big, bn * NAND_PAGE_SIZE)); + } + + /* Scattered sectors in two logical blocks five apart. Before log blocks + * this cost two whole block copies; now it should cost a couple of page + * appends, and only the eventual sync pays for folding them back. */ + { + static uint8_t sc[2][NAND_PAGE_SIZE]; + uint32_t lo = 60 * SBPAGES + 5, hi = 65 * SBPAGES + 7; + + for (i = 0; i < NAND_PAGE_SIZE; i++) + { + sc[0][i] = (uint8_t)(i * 3 + 1); + sc[1][i] = (uint8_t)(i * 9 + 2); + } + struct scat_ctx sctx = { { lo, hi }, { sc[0], sc[1] } }; + mock_reset_stats(); + check("a scattered write returns 0", + ftl_hook_write_merge(lo, hi, scat_src, &sctx) == 0); + printf(" scattered write of 2 sectors: %lu pages written\n", + mock_writes); + check("it appended pages instead of copying two blocks", + mock_writes < 16); + check("it raises no NAND violations", + mock_reprogram == 0 && mock_nonsequential == 0); + check("both scattered sectors read back", + ftl_read(lo, 1, buf) == 0 && !memcmp(buf, sc[0], NAND_PAGE_SIZE) + && ftl_read(hi, 1, buf) == 0 + && !memcmp(buf, sc[1], NAND_PAGE_SIZE)); + check("they survive a sync and remount", + ftl_sync() == 0 && ftl_init() == 0 + && ftl_read(lo, 1, buf) == 0 && !memcmp(buf, sc[0], NAND_PAGE_SIZE) + && ftl_read(hi, 1, buf) == 0 + && !memcmp(buf, sc[1], NAND_PAGE_SIZE)); + } + + /* An unclean shutdown: write, then remount without syncing. The merged + * blocks are not in the committed map, so the mount has to find them by + * scanning - exactly the case Apple's _FTLRestore exists for. */ + for (i = 0; i < n * NAND_PAGE_SIZE; i++) + pattern[i] = (uint8_t)(i * 13 + 5); + check("write before the simulated power loss", ftl_write(base, n, pattern) == 0); + rc = ftl_init(); + check("mounts after an unclean shutdown", rc == 0); + check("still writeable after an unclean shutdown", ftl_hook_writeable()); + check("the unsynced write is found by the log scan", + ftl_read(base, n, buf) == 0 + && !memcmp(buf, pattern, n * NAND_PAGE_SIZE)); + check("a later sync commits it", + ftl_sync() == 0 && ftl_init() == 0 + && ftl_read(base, n, buf) == 0 + && !memcmp(buf, pattern, n * NAND_PAGE_SIZE)); + + /* Regression: a superblock released by one merge must not be handed + * straight back to the next merge in the same uncommitted window. If it + * is, the committed map still names it for the first logical block while + * it now holds the second one's data, and after an unclean shutdown the + * first block reads the wrong data while the second is unrecoverable. + * Least-worn allocation made this reachable; FIFO only hid it. + * Every block written without a sync must survive the remount. */ + { + static uint8_t blk[12][NAND_PAGE_SIZE]; + uint32_t first = 200, nblk = 12, k, bad = 0; + + for (k = 0; k < nblk; k++) + memset(blk[k], (uint8_t)(0x40 + k), NAND_PAGE_SIZE); + for (k = 0; k < nblk; k++) + if (ftl_write((first + k) * SBPAGES + 9, 1, blk[k])) + bad++; + check("12 single-sector writes to 12 blocks, no sync", bad == 0); + { + uint32_t fc, fr, nl; + + ftl_pool_state(&fc, &fr, &nl); + printf(" %u of them still in uncommitted logs\n", nl); + check("some of them are still uncommitted", nl > 0); + } + check("remounts without a sync", ftl_init() == 0); + bad = 0; + for (k = 0; k < nblk; k++) + if (ftl_read((first + k) * SBPAGES + 9, 1, buf) + || memcmp(buf, blk[k], NAND_PAGE_SIZE)) + bad++; + check("every uncommitted block is recovered by the log scan", + bad == 0); + check("and a sync commits them all", + ftl_sync() == 0 && ftl_init() == 0); + bad = 0; + for (k = 0; k < nblk; k++) + if (ftl_read((first + k) * SBPAGES + 9, 1, buf) + || memcmp(buf, blk[k], NAND_PAGE_SIZE)) + bad++; + check("all 12 survive the commit", bad == 0); + } + + /* More merges than the pool has blocks: ftl_write_merge() must commit + * on its own rather than reuse a block released moments ago. */ + { + static uint8_t big2[NAND_PAGE_SIZE]; + uint32_t first = 400, nblk = 26, k, bad = 0; + + memset(big2, 0x5c, sizeof(big2)); + for (k = 0; k < nblk; k++) + if (ftl_write((first + k) * SBPAGES + 11, 1, big2)) + bad++; + check("26 merges - more than the 20-block pool - succeed", bad == 0); + check("remount after them", ftl_init() == 0); + bad = 0; + for (k = 0; k < nblk; k++) + if (ftl_read((first + k) * SBPAGES + 11, 1, buf) + || memcmp(buf, big2, NAND_PAGE_SIZE)) + bad++; + check("all 26 read back after the automatic commits", bad == 0); + check("final sync", ftl_sync() == 0 && ftl_init() == 0); + } + + /* A sync with nothing written since the last commit must not commit + * again: every shutdown calls it. ftl-nano2g.c returns early on its + * clean flag in the same way. */ + { + unsigned long w; + + check("sync before the idle-sync checks", ftl_sync() == 0); + w = mock_writes; + check("a sync with nothing written returns 0", ftl_sync() == 0); + check("... and programs no pages", mock_writes == w); + check("remount after a clean commit", ftl_init() == 0); + w = mock_writes; + check("a sync straight after remounting programs no pages", + ftl_sync() == 0 && mock_writes == w); + } + + /* One ftl_write_merge() call long enough to commit on its own partway + * through, and then keep writing. The commit leaves a clean context, so + * the dirty mark has to be written again before the later pages, or + * Apple's firmware would trust that context after an unclean stop and + * never look for them. */ + { + static uint8_t pg[NAND_PAGE_SIZE]; + struct every_ctx e = { 600, 20, 7, pg }; + uint32_t usn_before, usn_after, k, bad = 0; + int tail; + + memset(pg, 0x3b, sizeof(pg)); + check("clean before the long write", + ftl_sync() == 0 && apple_ctrl_tail(&usn_before) == 0x43); + check("a long scattered write returns 0", + ftl_hook_write_merge(e.first * SBPAGES, + (e.first + e.nblk) * SBPAGES - 1, + every_src, &e) == 0); + tail = apple_ctrl_tail(&usn_after); + check("it committed on its own partway through", + usn_after != usn_before); + check("pages written after that commit are flagged for Apple (0x4f)", + tail == 0x4f); + check("remount without syncing", ftl_init() == 0); + for (k = 0; k < e.nblk; k++) + if (ftl_read((e.first + k) * SBPAGES + e.idx, 1, buf) + || memcmp(buf, pg, NAND_PAGE_SIZE)) + bad++; + check("every block of the long write reads back", bad == 0); + check("sync after the long write", ftl_sync() == 0 && ftl_init() == 0); + } + + check_apple_invariants(usersb); + + printf(fails ? "SOME CHECKS FAILED\n" : "ALL PASS\n"); + return fails ? 1 : 0; +} diff --git a/utils/ipodnano3g/regdiff/.gitignore b/utils/ipodnano3g/regdiff/.gitignore new file mode 100644 index 0000000000..48f2762868 --- /dev/null +++ b/utils/ipodnano3g/regdiff/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +build/ +build-read/ diff --git a/utils/ipodnano3g/regdiff/fmss_dis.py b/utils/ipodnano3g/regdiff/fmss_dis.py new file mode 100644 index 0000000000..4d96860208 --- /dev/null +++ b/utils/ipodnano3g/regdiff/fmss_dis.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""fmss_dis.py [iram_end] -- disassemble an FMSS sequencer program. + +Apple's FIL runs programs on the S5L8702 flash controller's sequencer. They +live in IRAM (0x2200xxxx = osos file offset 0xxxxx). Instruction set as +decoded in ../decode/APPLE-FTL-READ.md: +8 bytes, word0 = op<<24 | rA<<16 | offset-or-rB, word1 = value. +""" +import struct, sys, os + +OSOS = os.environ.get("NANO3G_OSOS", os.path.join( + os.path.dirname(os.path.abspath(__file__)), "osos-1.1.3.bin")) + +FMC = { + 0x000: "FMCTRL0", 0x004: "FMCTRL1", 0x008: "FMCMD", 0x00c: "FMADDR0", + 0x010: "FMADDR1", 0x014: "FMADDR2", 0x018: "FMADDR3", 0x01c: "FMADDR4", + 0x020: "FMADDR5", 0x024: "FMADDR6", 0x028: "FMADDR7", 0x02c: "FMANUM", + 0x030: "FMDNUM", 0x034: "FMDATAW0", 0x038: "FMDATAW1", 0x03c: "FMDATAW2", + 0x040: "FMDATAW3", 0x044: "FMDATAW4?", 0x048: "FMCSTAT", 0x04c: "FMSYND0", + 0x06c: "FMSYND5", 0x070: "FMSYND6", 0x074: "FMSYND7", 0x078: "FMUNK78", + 0x07c: "FMUNK7C", 0x080: "FMFIFO", 0x80c: "FMTRANS0", 0x810: "FMUNK810", + 0x814: "FMTRANS1", 0x840: "FMTRANSSTAT", +} +EVENTS = {0: "CMDDONE(bit1)", 1: "ADDRDONE(bit2)", 2: "bit20", 3: "TRANSDONE(bit3)", + 4: "FMTRANS", 5: "STATUSREADY"} +ALU = {0x0a: "&", 0x0b: "|", 0x0c: "+", 0x0d: "-", 0x13: "<<", 0x14: ">>"} + + +def reg(off): + if off >= 0xc00: + return "SEQ+0x%03x" % off + return FMC.get(off, "FMC+0x%03x" % off) + + +def dis(start, end): + d = open(OSOS, "rb").read() + pc = start + while pc < end: + off = pc - 0x22000000 + w0, w1 = struct.unpack_from("> 24 + a = (w0 >> 16) & 0xff + b = w0 & 0xff + o = w0 & 0xffff + rel = pc - start + if op == 0x00: + txt = "end" + elif op == 0x01: + txt = "%s = 0x%x" % (reg(o), w1) + elif op == 0x02: + txt = "%s = r%d" % (reg(o), a) + elif op == 0x03: + txt = "r%d = mem32[r%d]" % (a, b) + elif op == 0x04: + txt = "r%d = %s & 0x%x" % (a, reg(o), w1) + elif op == 0x05: + txt = "r%d = 0x%x" % (a, w1) + elif op == 0x06: + txt = "r%d = r%d" % (a, b) + elif op == 0x07: + txt = "wait %s" % EVENTS.get(a, "event %d" % a) + elif op == 0x11: + txt = "mem32[r%d] = r%d" % (b, a) + elif op == 0x18: + txt = "r%d = seq32[r%d]" % (a, b) + elif op == 0x19: + txt = "seq32[r%d] = r%d" % (b, a) + elif op in ALU: + txt = ("r%d = r%d %s 0x%x" % (a, b, ALU[op], w1) if w1 + else "r%d %s= r%d" % (a, ALU[op], b)) + elif op == 0x0e: + txt = "if r%d != 0 goto +0x%x (0x%08x)" % (a, w1, start + w1) + elif op == 0x17: + txt = "if r%d == 0 goto +0x%x (0x%08x)" % (a, w1, start + w1) + else: + txt = "op %02x a%d b%d off 0x%x val 0x%x" % (op, a, b, o, w1) + print("%08x +%04x %08x %08x %s" % (pc, rel, w0, w1, txt)) + pc += 8 + + +if __name__ == "__main__": + s = int(sys.argv[1], 0) + e = int(sys.argv[2], 0) if len(sys.argv) > 2 else s + 0x400 + dis(s, e) diff --git a/utils/ipodnano3g/regdiff/fmss_emu.py b/utils/ipodnano3g/regdiff/fmss_emu.py new file mode 100644 index 0000000000..2d7b9ab89f --- /dev/null +++ b/utils/ipodnano3g/regdiff/fmss_emu.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""fmss_emu.py -- run an Apple FMSS sequencer program against a mock flash +controller and print the register trace. + +The mock controller always reports ready and pass, and the ECC encoder +always done, so the trace is the success path. Parameters are set up as +Apple's m1fmssWriteSequentialPages (0x082d430c) sets them for a run of pages. + + fmss_emu.py program2 NPAGES two-plane program 0x2200ac38 (mode 8) + fmss_emu.py program1 NPAGES single-plane program 0x22009e98 +""" +import struct, sys, os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from fmss_dis import OSOS, reg, EVENTS, ALU + +PROGRAMS = {"read": 0x22009030, "read-corrected": 0x22009030, + "program2": 0x2200ac38, + "program1": 0x22009e98} + + +class Emu: + def __init__(self, start, ecc_count=0): + self.d = open(OSOS, "rb").read() + self.start = start + self.r = [0] * 16 + self.io = {} # FMC and SEQ registers + self.mem = {} # parameter memory + self.trace = [] + self.raw = [] # (offset, value) of every FMC write + self.ecc_count = ecc_count + + def mem32(self, a): + return self.mem.get(a, 0) + + def rd(self, off): + if off == 0x810: # ECC result: bit 0 fail, bits 16..19 count + return self.ecc_count << 16 + if off == 0x07c: # encode/decode busy: always done + return 0 + if off == 0x04c: # FMSYND0: NAND status ready, pass + return 0x40 + if off == 0x048: # FMCSTAT + return 0xffffffff + return self.io.get(off, 0) + + def wr(self, off, v): + self.io[off] = v & 0xffffffff + if off < 0xc00: + self.trace.append("%-10s = 0x%x" % (reg(off), v & 0xffffffff)) + self.raw.append((off, v & 0xffffffff)) + + def run(self, limit=200000): + pc = self.start + steps = 0 + while steps < limit: + steps += 1 + w0, w1 = struct.unpack_from("> 24, (w0 >> 16) & 0xff, w0 & 0xff, w0 & 0xffff + r = self.r + nxt = pc + 8 + if op == 0x00: + return + elif op == 0x01: + self.wr(o, w1) + elif op == 0x02: + self.wr(o, r[a]) + elif op == 0x03: + r[a] = self.mem32(r[b]) + elif op == 0x04: + r[a] = self.rd(o) & w1 + elif op == 0x05: + r[a] = w1 + elif op == 0x06: + r[a] = r[b] + elif op == 0x07: + self.trace.append("wait %s" % EVENTS.get(a, a)) + elif op == 0x11: + self.mem[r[b]] = r[a] & 0xffffffff + elif op == 0x18: + r[a] = self.io.get(r[b], 0) + elif op == 0x19: + self.io[r[b]] = r[a] & 0xffffffff + elif op in ALU: + f = {0x0a: lambda x, y: x & y, 0x0b: lambda x, y: x | y, + 0x0c: lambda x, y: x + y, 0x0d: lambda x, y: x - y, + 0x13: lambda x, y: x << y, 0x14: lambda x, y: x >> y}[op] + r[a] = (f(r[b], w1) if w1 else f(r[a], r[b])) & 0xffffffff + elif op == 0x0e: + if r[a]: + nxt = self.start + w1 + elif op == 0x17: + if not r[a]: + nxt = self.start + w1 + else: + raise ValueError("op %02x at %08x" % (op, pc)) + pc = nxt + raise RuntimeError("step limit") + + +def setup_list(e, pages, base_ctrl0=0, units=1): + """pages: list of (bank, page) in the order the program is given them; + units: 2KiB units per page (1 on 2KiB-page chips, 2 on 4KiB)""" + base = 0x40000000 + pagelist, banklist, bufs, spares = base, base + 0x10000, base + 0x20000, base + 0x30000 + cetab, results = base + 0x40000, base + 0x50000 + for b in range(8): + e.mem[cetab + 4 * b] = b + for i, (bank, page) in enumerate(pages): + e.mem[pagelist + 4 * i] = page + e.mem[banklist + 4 * i] = bank + # One buffer per 2KiB unit, as the FILs build the list (0xD28 units) + for u in range(units): + e.mem[bufs + 4 * (i * units + u)] = (0x08000000 + i * 2048 * units + + u * 2048) + for w in range(3): + e.mem[spares + 4 * (i * 3 + w)] = 0x11110000 | (i << 4) | w + e.io.update({0xd04: 8, 0xd0c: pagelist, 0xd10: banklist, + 0xd14: cetab, 0xd18: len(pages), 0xd1c: spares, 0xd20: bufs, + 0xd24: results, 0xd28: units, 0xd48: base_ctrl0, + 0xd4c: 0, 0xd00: 64}) + + +def setup_write(e, npages, banks=4, planes=2): + """Pages in Apple's bank-major order for two-plane programming: for each + row, bank 0 plane 0, bank 0 plane 1, bank 1 plane 0, ...""" + base = 0x40000000 + pagelist, banklist, bufs, spares = base, base + 0x10000, base + 0x20000, base + 0x30000 + cetab = base + 0x40000 + per_row = banks * planes + for b in range(banks): + e.mem[cetab + 4 * b] = b # bank -> chip enable + for i in range(npages): + row, k = divmod(i, per_row) + bank, plane = divmod(k, planes) + block = 1000 + plane # a plane pair of blocks + e.mem[pagelist + 4 * i] = block * 128 + row + e.mem[banklist + 4 * i] = bank + for c in range(4): + e.mem[bufs + 4 * (i * 4 + c)] = 0x08000000 + i * 2048 + c * 512 + for w in range(3): + e.mem[spares + 4 * (i * 3 + w)] = 0x11110000 | (i << 4) | w + e.io.update({0xd04: per_row, 0xd0c: pagelist, 0xd10: banklist, + 0xd14: cetab, 0xd18: npages, 0xd1c: spares, 0xd20: bufs, + 0xd24: 0, 0xd28: 4, 0xd48: 0, 0xd4c: 0, 0xd00: 128 // 2}) + + +if __name__ == "__main__": + name = sys.argv[1] if len(sys.argv) > 1 else "program2" + n = int(sys.argv[2]) if len(sys.argv) > 2 else 8 + e = Emu(PROGRAMS[name], ecc_count=3 if name == "read-corrected" else 0) + if name.startswith("read"): + setup_list(e, [(i % 4, 1000 + i // 4) for i in range(n)]) + else: + setup_write(e, n) + e.run() + for t in e.trace: + print(t) + if name.startswith("read"): + results = e.io[0xd24] - 4 * n + print("results:", " ".join("0x%x" % e.mem[results + 4 * i] + for i in range(n))) diff --git a/utils/ipodnano3g/regdiff/regdiff.py b/utils/ipodnano3g/regdiff/regdiff.py new file mode 100644 index 0000000000..1016350987 --- /dev/null +++ b/utils/ipodnano3g/regdiff/regdiff.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""regdiff.py -- check nand_write_pages() in nand-nano3g.c against Apple's +FMSS write programs, register write for register write. + +The driver's write functions are extracted from the source, every FMC +register access is rewritten into a logging call, and the result is compiled +for the host with a mock controller that is always ready and never fails - +the same mock fmss_emu.py runs Apple's programs against. Both are given the +same run of pages and their register writes are compared. + + regdiff.py all scenarios +""" +import os, re, subprocess, sys, tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROCKBOX = os.path.abspath(os.path.join(HERE, "..", "..", "..")) +DRIVER = ROCKBOX + "/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c" +HEADER = ROCKBOX + "/firmware/export/s5l87xx.h" +sys.path.insert(0, HERE) +import fmss_emu + +FUNCS = ["nand_set_fmctrl0", "nand_wait_reg", "nand_wait_stat", + "nand_clear_status", "nand_send_cmd", "nand_send_addr_page", + "nand_read_status", "nand_finish", "nand_load_spare", "nand_encode", + "nand_run_status", "nand_write_pages"] + +# The base FMCTRL0 value nand_set_fmctrl0() uses without a chip enable +BASE_CTRL0 = 0x43000 + + +def registers(): + regs = {} + for m in re.finditer(r"#define\s+(FM[A-Z0-9_]+)\s+\(\*\(REG32_PTR_T\)" + r"\(FMC_BASE\s*\+\s*(0x[0-9A-Fa-f]+)\)\)", + open(HEADER).read()): + regs[m.group(1)] = int(m.group(2), 16) + return regs + + +def extract(src, name): + m = re.search(r"^(static\s+)?[a-z].*\b%s\(" % name, src, re.M) + if not m: + raise SystemExit("no function " + name) + i = src.index("{", m.start()) + depth = 0 + for j in range(i, len(src)): + if src[j] == "{": + depth += 1 + elif src[j] == "}": + depth -= 1 + if depth == 0: + return src[m.start():j + 1] + + +def transform(code, regs): + names = sorted(regs, key=len, reverse=True) + alt = "|".join(names) + + def reads(expr): + return re.sub(r"\b(%s)\b" % alt, lambda m: "R(0x%x)" % regs[m.group(1)], + expr) + + # nand_wait_reg takes a register offset on the host + code = code.replace("volatile uint32_t *reg", "uint32_t reg") + code = code.replace("*reg & bit", "R(reg) & bit") + code = code.replace("*reg = bit", "W(reg, bit)") + code = re.sub(r"&(%s)\b" % alt, lambda m: "0x%x" % regs[m.group(1)], code) + code = re.sub(r"\b(%s)\s*(\|=|&=)\s*([^;]+);" % alt, + lambda m: "W(0x%x, R(0x%x) %s (%s));" % ( + regs[m.group(1)], regs[m.group(1)], m.group(2)[0], + reads(m.group(3))), code) + code = re.sub(r"\b(%s)\s*=(?!=)\s*([^;]+);" % alt, + lambda m: "W(0x%x, %s);" % (regs[m.group(1)], + reads(m.group(2))), code) + return reads(code) + + +PRELUDE = r""" +#include +#include +#include +#include +#include +static uint32_t io[0x1000]; +static void W(uint32_t off, uint32_t v) { io[off] = v; printf("%03x=%08x\n", off, v); } +static uint32_t R(uint32_t off) +{ + if (off == 0x048 || off == 0x840) return 0xffffffff; /* every event done */ + if (off == 0x04c) return 0x40; /* ready, pass */ + if (off == 0x07c) return 0; /* encoder done */ + return io[off]; +} +#define USEC_TIMER 0UL +#define TIME_BEFORE(a, b) 1 +#define ARRAYLEN(a) (sizeof(a) / sizeof((a)[0])) +#define FMCTRL0_UNK1 (1 << 11) +#define FMCTRL0_AUTOXFER (1 << 24) +#define FMCTRL1_DOTRANSADDR (1 << 0) +#define FMCSTAT_CMDDONE (1 << 1) +#define FMCSTAT_ADDRDONE (1 << 2) +#define FMCSTAT_TRANSDONE (1 << 3) +#define FMCSTAT_UNK20 (1 << 20) +#define FMCSTAT_STATUSREADY (1 << 23) +#define NAND_PAGE_SIZE 2048 +#define NAND_CHUNK_SIZE 512 +#define NAND_OP_FAILED (-2) +struct nand_write { uint32_t bank, page; const void *buf; const uint32_t *meta; }; +struct { uint16_t blocks, pagesperblock, pagesize; } chip = { 4096, 128 }, + *nand_chip = &chip; +static bool nand_ready = true; +static unsigned int nand_banks = 4; +static void commit_dcache_range(const void *p, int n) { (void)p; (void)n; } +""" + + +def driver_harness(regs): + src = open(DRIVER).read() + defines = "\n".join(l for l in src.splitlines() + if re.match(r"#define\s+(NAND_CMD_|NAND_STATUS_|NAND_TUNK1|" + r"NAND_TWP|NAND_POLL_SPINS|NAND_MAX_BANKS)", l)) + protos = "\n".join(transform(re.sub(r"\)\s*$", ");", + extract(src, f).split("{")[0].strip()), regs) + for f in FUNCS) + body = "\n\n".join(transform(extract(src, f), regs) for f in FUNCS) + main = r""" +int main(int argc, char **argv) +{ + static struct nand_write w[64]; + static uint32_t meta[64][3]; + unsigned int n = 0, i; + int two = argv[1][0] == '2'; + chip.pagesize = atoi(argv[2]); + uint32_t bank, page, fb; + while (scanf("%u %u", &bank, &page) == 2) + { + w[n].bank = bank; w[n].page = page; + w[n].buf = (const void *)(intptr_t)(0x08000000 + n * chip.pagesize); + for (i = 0; i < 3; i++) meta[n][i] = 0x11110000 | (n << 4) | i; + w[n].meta = meta[n]; + n++; + } + return nand_write_pages(w, n, two, &fb) ? 1 : 0; +} +""" + return PRELUDE + defines + "\n" + protos + "\n" + body + main + + +def scenarios(): + def rows(nrows, pairs): + out = [] + for r in range(nrows): + if pairs: + for b in range(4): + out += [(b, 1000 * 128 + r), (b, 1001 * 128 + r)] + else: + for p in range(2): + for b in range(4): + out.append((b, (1000 + p) * 128 + r)) + return out + return [("one page", "1", [(0, 1000 * 128)]), + ("one page bank 2", "1", [(2, 1000 * 128 + 5)]), + ("single-plane row", "1", rows(1, False)), + ("single-plane two rows", "1", rows(2, False)), + ("three pages one bank", "1", [(1, 1000 * 128), (1, 1000 * 128 + 1), + (1, 1000 * 128 + 2)]), + ("two-plane row", "2", rows(1, True)), + ("two-plane two rows", "2", rows(2, True)), + ("two-plane three rows", "2", rows(3, True))] + + +def main(): + regs = registers() + tmp = os.path.join(HERE, "build") + os.makedirs(tmp, exist_ok=True) + c = os.path.join(tmp, "h.c") + exe = os.path.join(tmp, "h") + open(c, "w").write(driver_harness(regs)) + r = subprocess.run(["cc", "-std=gnu99", "-w", "-o", exe, c], + capture_output=True, text=True) + if r.returncode: + print(r.stderr[:3000]) + print("harness source:", c) + return 1 + fails = 0 + for (name, mode, pages), size in [(s, z) for z in (2048, 4096) + for s in scenarios()]: + name = "%s %dKiB" % (name, size // 1024) + e = fmss_emu.Emu(fmss_emu.PROGRAMS["program2" if mode == "2" else "program1"]) + fmss_emu.setup_list(e, pages, BASE_CTRL0 | 0x801, units=size // 2048) + e.run() + apple = ["%03x=%08x" % w for w in e.raw] + inp = "".join("%d %d\n" % p for p in pages) + out = subprocess.run([exe, mode, str(size)], input=inp, capture_output=True, + text=True).stdout.split() + # The driver clears FMCSTAT after every operation (nand_clear_status), + # which Apple's FIL does outside the program; that one trailing write + # is expected + if out[:-1] == apple and out[-1:] == ["048=ffffffff"]: + print("PASS %-30s %d register writes identical, + status clear" + % (name, len(apple))) + continue + fails += 1 + k = next((i for i, (a, b) in enumerate(zip(out, apple)) if a != b), + min(len(out), len(apple))) + print("FAIL %-30s first difference at write %d (ours %d, Apple %d)" + % (name, k, len(out), len(apple))) + for i in range(max(0, k - 6), min(max(len(out), len(apple)), k + 6)): + print(" %4d ours %-14s apple %s" % ( + i, out[i] if i < len(out) else "-", apple[i] if i < len(apple) else "-")) + print("ALL IDENTICAL" if not fails else "%d SCENARIOS DIFFER" % fails) + return 1 if fails else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/utils/ipodnano3g/regdiff/regdiff_read.py b/utils/ipodnano3g/regdiff/regdiff_read.py new file mode 100644 index 0000000000..6bfc56caf8 --- /dev/null +++ b/utils/ipodnano3g/regdiff/regdiff_read.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Compare nand_read_pages() register writes with Apple's read program.""" +import os +import re +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROCKBOX = os.path.abspath(os.path.join(HERE, "..", "..", "..")) +DRIVER = ROCKBOX + "/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c" +HEADER = ROCKBOX + "/firmware/export/s5l87xx.h" +sys.path.insert(0, HERE) +import fmss_emu + +FUNCS = ["nand_wait_reg", "nand_wait_stat", "nand_clear_status", + "nand_send_cmd", "nand_send_addr_page", "nand_read_status", + "nand_read_ecc_result", "nand_read_loaded_page", "nand_read_pages"] +BASE_CTRL0 = 0x43000 + + +def registers(): + regs = {} + with open(HEADER) as f: + src = f.read() + pattern = (r"#define\s+(FM[A-Z0-9_]+)\s+\(\*\(REG32_PTR_T\)" + r"\(FMC_BASE\s*\+\s*(0x[0-9A-Fa-f]+)\)\)") + for m in re.finditer(pattern, src): + regs[m.group(1)] = int(m.group(2), 16) + return regs + + +def extract(src, name): + m = re.search(r"^(static\s+)?[a-z].*\b%s\(" % name, src, re.M) + if not m: + raise SystemExit("no function " + name) + start = src.index("{", m.start()) + depth = 0 + for pos in range(start, len(src)): + if src[pos] == "{": + depth += 1 + elif src[pos] == "}": + depth -= 1 + if not depth: + return src[m.start():pos + 1] + + +def transform(code, regs): + alt = "|".join(sorted(regs, key=len, reverse=True)) + + def reads(expr): + return re.sub(r"\b(%s)\b" % alt, + lambda m: "R(0x%x)" % regs[m.group(1)], expr) + + code = code.replace("volatile uint32_t *reg", "uint32_t reg") + code = code.replace("*reg & bit", "R(reg) & bit") + code = code.replace("*reg = bit", "W(reg, bit)") + code = re.sub(r"&(%s)\b" % alt, + lambda m: "0x%x" % regs[m.group(1)], code) + code = re.sub(r"\b(%s)\s*(\|=|&=)\s*([^;]+);" % alt, + lambda m: "W(0x%x, R(0x%x) %s (%s));" % + (regs[m.group(1)], regs[m.group(1)], m.group(2)[0], + reads(m.group(3))), code) + code = re.sub(r"\b(%s)\s*=(?!=)\s*([^;]+);" % alt, + lambda m: "W(0x%x, %s);" % + (regs[m.group(1)], reads(m.group(2))), code) + return reads(code) + + +PRELUDE = r""" +#include +#include +#include +#include +static uint32_t io[0x1000]; +static void W(uint32_t off, uint32_t v) +{ io[off] = v; printf("%03x=%08x\n", off, v); } +static uint32_t R(uint32_t off) +{ + if (off == 0x048 || off == 0x840) return 0xffffffff; + if (off == 0x04c) return 0x40; + if (off == 0x07c || off == 0x810) return 0; + return io[off]; +} +#define USEC_TIMER 0UL +#define TIME_BEFORE(a, b) 1 +#define FMCTRL0_UNK1 (1 << 11) +#define FMCTRL0_AUTOXFER (1 << 24) +#define FMCTRL1_DOTRANSADDR (1 << 0) +#define FMCSTAT_CMDDONE (1 << 1) +#define FMCSTAT_ADDRDONE (1 << 2) +#define FMCSTAT_TRANSDONE (1 << 3) +#define FMCSTAT_UNK20 (1 << 20) +#define FMCSTAT_STATUSREADY (1 << 23) +#define FMCSTAT_UNK27 (1 << 27) +#define NAND_PAGE_SIZE 2048 +#define NAND_UNIT_SIZE 2048 +#define NAND_ECC_CLEAN 0 +#define NAND_ECC_CORRECTED 1 +#define NAND_ECC_FAILED 2 +struct nand_read { uint32_t bank, page; void *buf; uint32_t *meta; int ecc; + uint32_t result; }; +struct { uint16_t blocks, pagesperblock, pagesize; } chip = { 4096, 128 }; +static typeof(chip) *nand_chip = &chip; +static bool nand_ready = true; +static unsigned int nand_banks = 4; +static void commit_discard_dcache_range(const void *p, int n) +{ (void)p; (void)n; } +static void discard_dcache_range(const void *p, int n) +{ (void)p; (void)n; } +""" + + +def harness(regs): + with open(DRIVER) as f: + src = f.read() + defines = "\n".join(line for line in src.splitlines() + if re.match(r"#define\s+(NAND_CMD_|NAND_STATUS_|" + r"NAND_TUNK1|NAND_TWP|NAND_POLL_SPINS)", + line)) + protos = "\n".join(transform(re.sub(r"\)\s*$", ");", + extract(src, fn).split("{")[0].strip()), + regs) for fn in FUNCS) + body = "\n\n".join(transform(extract(src, fn), regs) for fn in FUNCS) + main = r""" +int main(int argc, char **argv) +{ + static struct nand_read r[64]; + static uint32_t meta[64][3]; + unsigned int n = 0; + uint32_t bank, page; + chip.pagesize = atoi(argv[1]); + while (scanf("%u %u", &bank, &page) == 2) + { + r[n].bank = bank; r[n].page = page; + r[n].buf = (void *)(intptr_t)(0x08000000 + n * chip.pagesize); + r[n].meta = meta[n]; + n++; + } + return nand_read_pages(r, n) ? 1 : 0; +} +""" + return PRELUDE + defines + "\n" + protos + "\n" + body + main + + +def scenarios(): + return [("one page", [(0, 128000)]), + ("four banks", [(i, 128000) for i in range(4)]), + ("same bank", [(0, 128000 + i) for i in range(3)]), + ("duplicate split", [(0, 128000), (1, 128000), + (0, 128001), (2, 128000)])] + + +def main(): + regs = registers() + build = os.path.join(HERE, "build-read") + os.makedirs(build, exist_ok=True) + cfile, exe = os.path.join(build, "h.c"), os.path.join(build, "h") + with open(cfile, "w") as f: + f.write(harness(regs)) + cc = subprocess.run(["cc", "-std=gnu99", "-w", "-o", exe, cfile], + capture_output=True, text=True) + if cc.returncode: + print(cc.stderr[:4000]) + return 1 + failures = 0 + for (name, pages), size in [(s, z) for z in (2048, 4096) + for s in scenarios()]: + name = "%s %dKiB" % (name, size // 1024) + emu = fmss_emu.Emu(fmss_emu.PROGRAMS["read"]) + fmss_emu.setup_list(emu, pages, units=size // 2048) + emu.io[0xd4c] = BASE_CTRL0 + emu.run() + apple = ["%03x=%08x" % item for item in emu.raw] + inp = "".join("%d %d\n" % item for item in pages) + ours = subprocess.run([exe, str(size)], input=inp, + capture_output=True, + text=True).stdout.split() + if ours == apple: + print("PASS %-26s %d register writes identical" % + (name, len(apple))) + continue + failures += 1 + pos = next((i for i, pair in enumerate(zip(ours, apple)) + if pair[0] != pair[1]), min(len(ours), len(apple))) + print("FAIL %-26s first difference %d (ours %d, Apple %d)" % + (name, pos, len(ours), len(apple))) + for i in range(max(0, pos - 5), min(max(len(ours), len(apple)), + pos + 8)): + print(" %4d ours %-14s apple %s" % + (i, ours[i] if i < len(ours) else "-", + apple[i] if i < len(apple) else "-")) + print("ALL IDENTICAL" if not failures else + "%d SCENARIOS DIFFER" % failures) + return bool(failures) + + +if __name__ == "__main__": + sys.exit(main()) From 9ee5873770c881be63fb7df94c7128060f933972 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Sat, 19 Sep 2026 07:52:36 +1000 Subject: [PATCH 195/408] utils: update the Nano 3G NAND check tool The contributor NAND check tool went in before the driver changes that followed it. This brings it up to the current tree. Testing evidence: utils/ipodnano3g/RESULTS.md. Co-authored-by: Claude Opus 5 Change-Id: Ie7712e0d7eaf75b880a9d134715ce1396a8545f8 --- utils/ipodnano3g/RESULTS.md | 6 ++ utils/ipodnano3g/nandcheck/README.md | 87 ++++++++++++++------ utils/ipodnano3g/nandcheck/nandcheck.py | 10 ++- utils/ipodnano3g/nandcheck/nano3g-check.dfu | Bin 88140 -> 88428 bytes 4 files changed, 72 insertions(+), 31 deletions(-) diff --git a/utils/ipodnano3g/RESULTS.md b/utils/ipodnano3g/RESULTS.md index ba565bd8ed..5ae1dc4ee7 100644 --- a/utils/ipodnano3g/RESULTS.md +++ b/utils/ipodnano3g/RESULTS.md @@ -42,3 +42,9 @@ implemented layout and recovery order follow the decoded Apple FTL/VFL process documented in `decode/APPLE-FTL-WRITE.md`; that correspondence is evidence for the implementation, not a substitute for destructive hardware fault testing. + +## Later additions + +- **NAND check tool update, 2026-09-17.** The rebuilt check image reads a + 4GB unit with no ECC failures or timeouts; two independent collections + from it agree byte for byte. diff --git a/utils/ipodnano3g/nandcheck/README.md b/utils/ipodnano3g/nandcheck/README.md index bb6024b584..6b7ee64658 100644 --- a/utils/ipodnano3g/nandcheck/README.md +++ b/utils/ipodnano3g/nandcheck/README.md @@ -1,9 +1,9 @@ # Nano 3G NAND check -Apple fitted many different NAND chips to the iPod Nano 3G. Rockbox's Nano -3G NAND support is validated chip by chip: it writes only to chips that have -been tested on hardware, and keeps the storage read-only on the rest. This -check collects what we need to validate your chip. It **changes nothing on your +Apple fitted many different NAND chips to the iPod Nano 3G. Rockbox runs on +all of them, but it only writes to chips that have been validated on +hardware; on the rest the player's storage is read-only. This check +collects what we need to validate your chip. It **changes nothing on your iPod**: nothing is installed, and nothing is written to its NAND. The image runs from RAM and is gone when the iPod restarts. @@ -13,7 +13,8 @@ Every Nano 3G is model A1236, whichever NAND it has, so the chip has to be read from the iPod itself: 1. **Capacity** narrows it down: Settings > About in Apple's firmware - shows 4GB or 8GB, and each row below is one capacity. + shows 4GB or 8GB, and each row below is one capacity. Both Hynix + parts, the 4GB and the 8GB, are validated. 2. **The check's screen** names it exactly, within seconds and without changing anything: run steps 1 and 2 below and read the `id` line (the chip, as in the table) and the `banks` line (how many chip enables @@ -25,37 +26,42 @@ the iPod. Otherwise please carry on with steps 3 and 4. These are all the chips Apple's firmware (1.1.3) supports. Apple picks the row by the id and the number of chip enables, so one id can appear twice. -Rockbox's NAND driver knows every row, but apart from the validated chip -it has so far been tested only against simulated NAND on a computer, which -is why the archives matter. +Rockbox supports every row, but writes only to validated ones; on the +others the storage is read-only. Apart from the validated chip, that +support has so far been tested only against simulated NAND on a computer, +which is why the archives matter. | id | maker | capacity | chip enables | page | status | |---|---|---|---|---|---| | `A514D3AD` | Hynix | 4GB | 4 | 2KiB | **validated** | -| `A555D5AD` | Hynix | 8GB | 4 | 2KiB | reported, needs a check | +| `A555D5AD` | Hynix | 8GB | 4 | 2KiB | **validated** | | `B614D5AD` | Hynix | 8GB | 4 | 4KiB | needed | -| `B614D5EC` | Samsung | 4GB | 2 | 4KiB | reported, needs a check | -| `B614D5EC` | Samsung | 8GB | 4 | 4KiB | needed | -| `2555D5EC` | Samsung | 8GB | 4 | 2KiB | needed | +| `B614D5EC` | Micronas | 4GB | 2 | 4KiB | checked | +| `B614D5EC` | Micronas | 8GB | 4 | 4KiB | needed | +| `2555D5EC` | Micronas | 8GB | 4 | 2KiB | needed | | `A585D598` | Toshiba | 4GB | 2 | 2KiB | needed | | `A585D598` | Toshiba | 8GB | 4 | 2KiB | needed | | `BA94D598` | Toshiba | 4GB | 2 | 4KiB | needed | | `BA94D598` | Toshiba | 8GB | 4 | 4KiB | needed | -| `A5D5D589` | Intel | 4GB | 2 | 2KiB | reported, needs a check | -| `A5D5D589` | Intel | 8GB | 4 | 2KiB | reported, needs a check | -| `3E94D589` | Intel | 4GB | 2 | 4KiB | reported, needs a check | +| `A5D5D589` | Intel | 4GB | 2 | 2KiB | checked | +| `A5D5D589` | Intel | 8GB | 4 | 2KiB | checked | +| `3E94D589` | Intel | 4GB | 2 | 4KiB | checked | | `3ED5D789` | Intel | 8GB | 2 | 4KiB | needed | -| `A5D5D52C` | Micron | 4GB | 2 | 2KiB | needed | +| `A5D5D52C` | Micron | 4GB | 2 | 2KiB | **validated** | | `A5D5D52C` | Micron | 8GB | 4 | 2KiB | needed | | `3E94D52C` | Micron | 4GB | 2 | 4KiB | needed | | `3ED5D72C` | Micron | 8GB | 2 | 4KiB | needed | -"Reported" means someone has seen that chip in a unit, but no check archive -has been collected from it yet. The id is the first four READ ID bytes read -as one little-endian number: `A514D3AD` is the bytes `AD D3 14 A5`, and the -last byte is the maker (`AD` Hynix, `EC` Samsung, `98` Toshiba, `89` Intel, -`2C` Micron). When a chip is validated, the driver's chip table and this -table are updated together. +"Needed" means nobody has reported a unit with that chip yet. "Reported" +means someone has, but no check archive has been collected from it. +"Checked" means an archive has, and it mounts and replays correctly in the +host FTL suite - short only of the on-device write test that validation +needs. The id is the first four READ ID bytes read as one little-endian +number: `A514D3AD` is the bytes `AD D3 14 A5`, and the last byte is the +maker (`AD` Hynix, `EC` Micronas, `98` Toshiba, `89` Intel, `2C` Micron). +When a chip is validated, its row in `nand_chip_table[]` +(`firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c`) and this table +are updated together. ## What you need @@ -67,10 +73,35 @@ table are updated together. ## 1. Get the image -`nano3g-check.dfu` in this directory is ready to use. It is the Nano 3G -bootloader built with `-DNAND_CHECK` from the Nano 3G NAND driver work, -which is not in Rockbox yet; its build shows on the iPod's screen as -"Version:" and in the report as `version`. +`nano3g-check.dfu` in this directory is ready to use. Its build shows on +the iPod's screen as "Version:" and in the report as `version`, which is +the commit it was built from. + +To build it yourself instead (an ARM toolchain: `tools/rockboxdev.sh`, or +your distribution's `arm-none-eabi-gcc`): + +``` +utils/ipodnano3g/nandcheck/build.sh check +``` + +This writes `nano3g-check.dfu` in the current directory (and +`nano3g-check-wind3x.dfu` if `wInd3x` is installed). + +### Where the code is + +The image is the Nano 3G bootloader built with `-DNAND_CHECK`: + +| part | file | +|---|---| +| the screen, SysCfg and USB mode | `nand_check()` in `bootloader/ipod-s5l87xx.c` | +| chip identification, the report and the raw USB view | the `NAND_CHECK` block of `firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c` | +| Apple's chip table | `nand_chip_table[]` in the same file | +| the read-only mount it reports | `firmware/target/arm/s5l8702/ipodnano3g/ftl-nano3g.c` | +| the build | `build.sh` in this directory | + +`nandcheck.py`'s module comment documents the disk layout the image +serves, and `utils/ipodnano3g/README` describes the host tests that a +collected archive feeds. ## 2. Run it @@ -79,6 +110,8 @@ which is not in Rockbox yet; its build shows on the iPod's screen as ``` utils/mks5lboot/mks5lboot --dfusend utils/ipodnano3g/nandcheck/nano3g-check.dfu ``` + (With wInd3x instead: `wInd3x haxdfu`, then + `wInd3x run nano3g-check-wind3x.dfu`, built as above.) 3. Within a few seconds the iPod shows "Nano 3G NAND check" and a short summary: `battery`, `id` (the chip), `banks`, `row`, `mode`, `pagesize`, `validated`, `ftl`, `verdict`, `model` and `swvr`. Then "Bootloader USB @@ -126,7 +159,7 @@ If the verdict says the chip is not in Apple's table, the photo is enough. | file | contents | |---|---| -| `report.txt` | chip ids (all eight id bytes, every chip enable), chip table row and derived geometry, read-only mount result, verdict, model, hardware and firmware version, battery voltage, backlight register | +| `report.txt` | chip ids (all eight id bytes, every chip enable), chip table row and derived geometry, read-only mount result, verdict, model, hardware and firmware version, battery voltage | | `meta.bin` | every page's 12 spare bytes and read result, bank by bank (the host tests' dump format) | | `results.bin` | every page's full read result: ECC class, most bits corrected in a chunk, erased flag | | `pages.bin` | every page that is not user data or erased: FTL and VFL control structures, Apple's bad-block records | diff --git a/utils/ipodnano3g/nandcheck/nandcheck.py b/utils/ipodnano3g/nandcheck/nandcheck.py index bed873131e..0cbfdc8f3c 100644 --- a/utils/ipodnano3g/nandcheck/nandcheck.py +++ b/utils/ipodnano3g/nandcheck/nandcheck.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 r"""Collect what is needed to validate an iPod Nano 3G's NAND for Rockbox. -1. Put the iPod in DFU mode and run the check image: - mks5lboot --dfusend nano3g-check.dfu +1. Build the check image (build.sh check), put the iPod in DFU mode and run + it: mks5lboot --dfusend nano3g-check.dfu + (With wInd3x, an external tool not part of Rockbox: wInd3x haxdfu, then + wInd3x run nano3g-check-wind3x.dfu, built the same way.) The screen shows the chip, its table row and how a read-only mount went. Nothing is written to the iPod. 2. Connect USB. The iPod appears as a disk of 2048-byte sectors. @@ -14,11 +16,11 @@ r"""Collect what is needed to validate an iPod Nano 3G's NAND for Rockbox. This writes nandcheck--x.tar.gz: the report, the spare metadata of every page and every page that is not file data - the VFL and FTL control structures and Apple's bad-block records. No file - contents are included. It takes about 7 minutes on a 4GB unit. + contents are included. It takes 10-20 minutes. Add --full to include every page's data as well (4-8 GB, and it contains whatever is stored on the iPod). -The check image serves: +The check image serves (nand-check-nano3g.c in the Nano 3G target): sector 0 text report 1 + block * banks + bank 128 records of 16 bytes: 3 spare words and a result word, little endian: -1 on diff --git a/utils/ipodnano3g/nandcheck/nano3g-check.dfu b/utils/ipodnano3g/nandcheck/nano3g-check.dfu index e540cc222a4ae167377f0894f4b1f3a298ebd68b..aba5e1291bbdabe24c96cad676a9eb6e95bb5bea 100644 GIT binary patch delta 12894 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 Kh?WfM|9=2dyPnJd From 01925dd5d08ff33a928cc4691fe71e2aa51065e2 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Sat, 19 Sep 2026 07:52:53 +1000 Subject: [PATCH 196/408] ipodnano3g: enable the 2-CE Micron A5D5D52C NAND A contributor's check archive for a 2-chip-enable A5D5D52C unit matches the row exactly and replays clean against the host FTL suite. The row moves in with the validated chips on this evidence alone - no on-device write test has been run. Testing evidence: utils/ipodnano3g/RESULTS.md. Co-authored-by: Claude Opus 5 Change-Id: Ia9e00c0e880b10785da3ab52bc98dc93e94acfaf --- firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c | 3 ++- utils/ipodnano3g/RESULTS.md | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c index 4b5c5b699f..bd46c54451 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c @@ -111,6 +111,8 @@ static const struct nand_chip_info nand_chip_table[] = /* Hynix: the 4GB unit this port was developed on, and the 8GB one */ { 0xA514D3AD, 4, 8, 4096, 128, 2048, 3872, true }, { 0xA555D5AD, 4, 8, 8192, 128, 2048, 7744, true }, + /* Micron, 2 chip enables */ + { 0xA5D5D52C, 2, 4, 8192, 128, 2048, 7744, true }, #ifdef NAND_CHECK /* The rest of the chips the original firmware knows. They are here for * the check image alone, which identifies a chip and reads it to @@ -135,7 +137,6 @@ static const struct nand_chip_info nand_chip_table[] = { 0x3E94D589, 2, 3, 4096, 128, 4096, 3872, false }, /* reported */ { 0x3ED5D789, 2, 2, 8192, 128, 4096, 7744, false }, /* Micron */ - { 0xA5D5D52C, 2, 4, 8192, 128, 2048, 7744, false }, { 0xA5D5D52C, 4, 2, 8192, 128, 2048, 7744, false }, { 0x3E94D52C, 2, 3, 4096, 128, 4096, 3872, false }, { 0x3ED5D72C, 2, 2, 8192, 128, 4096, 7744, false }, diff --git a/utils/ipodnano3g/RESULTS.md b/utils/ipodnano3g/RESULTS.md index 5ae1dc4ee7..4c254fabf7 100644 --- a/utils/ipodnano3g/RESULTS.md +++ b/utils/ipodnano3g/RESULTS.md @@ -48,3 +48,8 @@ fault testing. - **NAND check tool update, 2026-09-17.** The rebuilt check image reads a 4GB unit with no ECC failures or timeouts; two independent collections from it agree byte for byte. +- **2-CE Micron A5D5D52C, enabled 2026-09-18.** A contributor's check + archive matches the table row exactly, including a derived `vflspares` + of 89. Replayed against the host suite: `test_ftl` agrees with the + oracle on all 1,982,464 sectors; `test_crash` survives 100 power cuts + (172 writes, 6 syncs, 0 sectors wrong). No on-device write test. From 00829f22587a5e7ae9962306f92e71f3dbd99c6f Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Sun, 20 Sep 2026 09:29:37 +1000 Subject: [PATCH 197/408] ipodnano3g: mark four contributors' chips checked Four check archives (B614D5EC x2, A5D5D589 x2, A5D5D589 x4, 3E94D589 x2) all match their table rows and pass test_crash clean. Three pass test_ftl outright; A5D5D589 x2 disagrees with the oracle on two logical pages, traced to _FTLRestore's own tie-break between two competing logs (verified instruction-for-instruction against osos 1.1.3) - Apple's own firmware would resolve the same medium the same way, so this is not a defect. test_ftl.c gets a note explaining the false positive for future archives that hit it. Co-authored-by: Claude Opus 5 Change-Id: If8de0d6b1175981a292c5c45fbc092d5a6e0891a --- .../target/arm/s5l8702/ipodnano3g/nand-nano3g.c | 14 +++++++++----- utils/ipodnano3g/RESULTS.md | 6 ++++++ utils/ipodnano3g/ftltest/test_ftl.c | 10 ++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c index bd46c54451..1ac24502d0 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c @@ -119,9 +119,13 @@ static const struct nand_chip_info nand_chip_table[] = * collect what validating it needs; a normal build does not carry them, * so Rockbox cannot drive a chip nobody has tested. A row moves above * this line, with its validated flag set, once someone has run the - * check and a write test on that chip. */ + * check and a write test on that chip. A row tagged "reported" is + * secondhand - someone's id and geometry, unconfirmed by us; one + * tagged "checked" has a check archive that mounts and replays + * correctly in the host FTL suite - real evidence, short only of the + * write test. */ /* Micronas (ITT Intermetall, acquired by TDK 2016): 0xEC, not Samsung */ - { 0xB614D5EC, 2, 8, 4096, 128, 4096, 3872, false }, /* reported */ + { 0xB614D5EC, 2, 8, 4096, 128, 4096, 3872, false }, /* checked */ { 0xB614D5EC, 4, 1, 4096, 128, 4096, 3872, false }, { 0x2555D5EC, 4, 9, 8192, 128, 2048, 7744, false }, /* Hynix */ @@ -132,9 +136,9 @@ static const struct nand_chip_info nand_chip_table[] = { 0xBA94D598, 2, 12, 4096, 128, 4096, 3872, false }, { 0xBA94D598, 4, 1, 4096, 128, 4096, 3872, false }, /* Intel */ - { 0xA5D5D589, 2, 4, 8192, 128, 2048, 7744, false }, /* reported */ - { 0xA5D5D589, 4, 2, 8192, 128, 2048, 7744, false }, /* reported */ - { 0x3E94D589, 2, 3, 4096, 128, 4096, 3872, false }, /* reported */ + { 0xA5D5D589, 2, 4, 8192, 128, 2048, 7744, false }, /* checked */ + { 0xA5D5D589, 4, 2, 8192, 128, 2048, 7744, false }, /* checked */ + { 0x3E94D589, 2, 3, 4096, 128, 4096, 3872, false }, /* checked */ { 0x3ED5D789, 2, 2, 8192, 128, 4096, 7744, false }, /* Micron */ { 0xA5D5D52C, 4, 2, 8192, 128, 2048, 7744, false }, diff --git a/utils/ipodnano3g/RESULTS.md b/utils/ipodnano3g/RESULTS.md index 4c254fabf7..c81be13dfa 100644 --- a/utils/ipodnano3g/RESULTS.md +++ b/utils/ipodnano3g/RESULTS.md @@ -53,3 +53,9 @@ fault testing. of 89. Replayed against the host suite: `test_ftl` agrees with the oracle on all 1,982,464 sectors; `test_crash` survives 100 power cuts (172 writes, 6 syncs, 0 sectors wrong). No on-device write test. +- **B614D5EC x2, A5D5D589 x2/x4, 3E94D589 x2, 2026-09-19.** Four + contributors' check archives all match their rows and pass `test_crash` + clean. `test_ftl` passes three outright; A5D5D589 x2 disagrees with the + oracle on two logical pages - confirmed against Apple's own disassembly + (`_FTLRestore`, osos 1.1.3) to be that algorithm's own tie-break between + two competing logs, not a defect (see the note in `ftltest/test_ftl.c`). diff --git a/utils/ipodnano3g/ftltest/test_ftl.c b/utils/ipodnano3g/ftltest/test_ftl.c index 5366bdc28e..785bbfd0dd 100644 --- a/utils/ipodnano3g/ftltest/test_ftl.c +++ b/utils/ipodnano3g/ftltest/test_ftl.c @@ -7,6 +7,16 @@ * says "user data, this lpn", the newest - highest usn, then the latest * position in its superblock. Every lpn the FTL resolves must land on the * oracle's copy (or, where copies tie, on one with identical data). + * + * Known false positive: when two logs for the same logical block each hold + * pages the other lacks, _FTLRestore's own tie-break (0x806a19c in osos + * 1.1.3, matched here instruction for instruction) keeps the more complete + * one whole and drops the other entirely, even if the dropped one is newer + * for some of its pages. ftl_read() then disagrees with this oracle on + * exactly those pages - correctly, since that is what Apple's own firmware + * would also resolve to on the same medium. A handful of disagreements + * confined to one or two logical blocks on a real contributor's dump is + * this, not a bug; wholesale disagreement is not. */ #include #include From 413f17b8ceecede42d2dce543c0f4c99d1c8157e Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Sun, 20 Sep 2026 11:50:54 +1000 Subject: [PATCH 198/408] ipodnano3g: mark the 4-CE Toshiba BA94D598 row checked A contributor's check archive matches the row exactly and passes both host tests clean: test_ftl agrees with the oracle on all 3,964,928 sectors, test_crash survives 100 power cuts with 0 sectors wrong. Co-authored-by: Claude Opus 5 Change-Id: I7c651c94332e0a04686ce8855a274735ae39546c --- firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c | 2 +- utils/ipodnano3g/RESULTS.md | 5 +++++ utils/ipodnano3g/nandcheck/README.md | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c index 1ac24502d0..be22227236 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c @@ -134,7 +134,7 @@ static const struct nand_chip_info nand_chip_table[] = { 0xA585D598, 2, 13, 8320, 128, 2048, 7744, false }, { 0xA585D598, 4, 13, 8320, 128, 2048, 7744, false }, { 0xBA94D598, 2, 12, 4096, 128, 4096, 3872, false }, - { 0xBA94D598, 4, 1, 4096, 128, 4096, 3872, false }, + { 0xBA94D598, 4, 1, 4096, 128, 4096, 3872, false }, /* checked */ /* Intel */ { 0xA5D5D589, 2, 4, 8192, 128, 2048, 7744, false }, /* checked */ { 0xA5D5D589, 4, 2, 8192, 128, 2048, 7744, false }, /* checked */ diff --git a/utils/ipodnano3g/RESULTS.md b/utils/ipodnano3g/RESULTS.md index c81be13dfa..dec077a7fa 100644 --- a/utils/ipodnano3g/RESULTS.md +++ b/utils/ipodnano3g/RESULTS.md @@ -59,3 +59,8 @@ fault testing. oracle on two logical pages - confirmed against Apple's own disassembly (`_FTLRestore`, osos 1.1.3) to be that algorithm's own tie-break between two competing logs, not a defect (see the note in `ftltest/test_ftl.c`). +- **BA94D598 x4, 2026-09-20.** A contributor's check archive matches the + row exactly (Toshiba, mode 1, vflspares 201 by our own formula). Clean + on both host tests: `test_ftl` agrees with the oracle on all 3,964,928 + sectors; `test_crash` survives 100 power cuts (344 writes, 11 syncs, 0 + sectors wrong). No on-device write test. diff --git a/utils/ipodnano3g/nandcheck/README.md b/utils/ipodnano3g/nandcheck/README.md index 6b7ee64658..69eb899812 100644 --- a/utils/ipodnano3g/nandcheck/README.md +++ b/utils/ipodnano3g/nandcheck/README.md @@ -42,7 +42,7 @@ which is why the archives matter. | `A585D598` | Toshiba | 4GB | 2 | 2KiB | needed | | `A585D598` | Toshiba | 8GB | 4 | 2KiB | needed | | `BA94D598` | Toshiba | 4GB | 2 | 4KiB | needed | -| `BA94D598` | Toshiba | 8GB | 4 | 4KiB | needed | +| `BA94D598` | Toshiba | 8GB | 4 | 4KiB | checked | | `A5D5D589` | Intel | 4GB | 2 | 2KiB | checked | | `A5D5D589` | Intel | 8GB | 4 | 2KiB | checked | | `3E94D589` | Intel | 4GB | 2 | 4KiB | checked | From bd24fddb7ef2897e42b1efcb9fc2d0766cee5aee Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Mon, 21 Sep 2026 21:53:52 +1000 Subject: [PATCH 199/408] ipodnano3g: mark the 4-CE B614D5EC row checked A contributor's check archive matches the row exactly and passes test_crash clean. test_ftl disagrees with the oracle on two logical pages in one block - a second, distinct false positive from the A5D5D589 x2 case: a closed data block addressed purely by position, with one stale leftover page. Confirmed against the decode notes (_FTLRestore's "closed blocks -> map" step) and documented in test_ftl.c alongside the existing false positive. Testing evidence: utils/ipodnano3g/RESULTS.md. Co-authored-by: Claude Opus 5 Change-Id: Ib24d2df18e2b0e60e000ee6ea43bef9c214f7f72 --- .../arm/s5l8702/ipodnano3g/nand-nano3g.c | 2 +- utils/ipodnano3g/RESULTS.md | 9 ++++++ utils/ipodnano3g/ftltest/test_ftl.c | 32 +++++++++++++------ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c b/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c index be22227236..28766045b1 100644 --- a/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c +++ b/firmware/target/arm/s5l8702/ipodnano3g/nand-nano3g.c @@ -126,7 +126,7 @@ static const struct nand_chip_info nand_chip_table[] = * write test. */ /* Micronas (ITT Intermetall, acquired by TDK 2016): 0xEC, not Samsung */ { 0xB614D5EC, 2, 8, 4096, 128, 4096, 3872, false }, /* checked */ - { 0xB614D5EC, 4, 1, 4096, 128, 4096, 3872, false }, + { 0xB614D5EC, 4, 1, 4096, 128, 4096, 3872, false }, /* checked */ { 0x2555D5EC, 4, 9, 8192, 128, 2048, 7744, false }, /* Hynix */ { 0xB614D5AD, 4, 1, 4096, 128, 4096, 3872, false }, diff --git a/utils/ipodnano3g/RESULTS.md b/utils/ipodnano3g/RESULTS.md index dec077a7fa..c7e8166ea8 100644 --- a/utils/ipodnano3g/RESULTS.md +++ b/utils/ipodnano3g/RESULTS.md @@ -64,3 +64,12 @@ fault testing. on both host tests: `test_ftl` agrees with the oracle on all 3,964,928 sectors; `test_crash` survives 100 power cuts (344 writes, 11 syncs, 0 sectors wrong). No on-device write test. +- **B614D5EC x4, 2026-09-21.** A contributor's check archive matches the + row exactly (mode 1, vflspares 201 by our own formula). `test_crash` is + clean (100 power cuts, 349 writes, 9 syncs, 0 sectors wrong). `test_ftl` + disagrees with the oracle on two logical pages in one block; traced to a + second, distinct false positive from the one already documented for + A5D5D589 x2 - a closed data block addressed purely by position, with one + stale leftover page - confirmed against the decode notes + (`_FTLRestore`'s "closed blocks -> map" step) and not a defect (see + `ftltest/test_ftl.c`). diff --git a/utils/ipodnano3g/ftltest/test_ftl.c b/utils/ipodnano3g/ftltest/test_ftl.c index 785bbfd0dd..0c3aa0389e 100644 --- a/utils/ipodnano3g/ftltest/test_ftl.c +++ b/utils/ipodnano3g/ftltest/test_ftl.c @@ -8,15 +8,29 @@ * position in its superblock. Every lpn the FTL resolves must land on the * oracle's copy (or, where copies tie, on one with identical data). * - * Known false positive: when two logs for the same logical block each hold - * pages the other lacks, _FTLRestore's own tie-break (0x806a19c in osos - * 1.1.3, matched here instruction for instruction) keeps the more complete - * one whole and drops the other entirely, even if the dropped one is newer - * for some of its pages. ftl_read() then disagrees with this oracle on - * exactly those pages - correctly, since that is what Apple's own firmware - * would also resolve to on the same medium. A handful of disagreements - * confined to one or two logical blocks on a real contributor's dump is - * this, not a bug; wholesale disagreement is not. + * Known false positive #1: when two logs for the same logical block each + * hold pages the other lacks, _FTLRestore's own tie-break (0x806a19c in + * osos 1.1.3, matched here instruction for instruction) keeps the more + * complete one whole and drops the other entirely, even if the dropped one + * is newer for some of its pages. ftl_read() then disagrees with this + * oracle on exactly those pages - correctly, since that is what Apple's + * own firmware would also resolve to on the same medium. + * + * Known false positive #2: a superblock whose last page reads + * SPARE_DATA_LAST is classified a closed data block from that one page + * alone ("closed blocks -> map" in decode/APPLE-FTL-WRITE.md) and every + * lpn in it is then addressed purely by position, offset == lpn % sbpages, + * with no per-page check. If one physical page in such a block is stale - + * left over from before the block closed, still carrying an older lpn's + * spare - the oracle credits that spare's lpn with the newest copy at the + * position it actually holds data, while the FTL (like Apple's own + * firmware) trusts the position instead: it lands on the stale page for + * the lpn that owns that spare, and finds nothing at the position the + * oracle expected for the lpn that really belongs there. + * + * Both are confined to a handful of pages on one or two logical blocks on + * a real contributor's dump; wholesale disagreement is not either of + * these, and is a bug. */ #include #include From b18f5d6d65bcbeaf82490105b14b0e636d6a4f1f Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 11 Sep 2026 22:04:40 -0400 Subject: [PATCH 200/408] opus: ARMv4 assembly FFT butterflies, and an exact radix-5 rewrite Cuts realtime Opus decode on the Sansa e200v1 (PP502x, ARM7TDMI) from 55.55 MHz to 52.1 MHz. Two exact changes to celt/kiss_fft.c first: - kf_bfly5 folds the four cosine products into a shift and a single multiply. cos(2*pi/5) + cos(4*pi/5) is exactly -1/2, and the Q15 constants satisfy that identity exactly (10126 - 26510 == -16384), so the substitution gives up no accuracy. - kf_bfly3, kf_bfly4 and kf_bfly5 peel the pass that twiddles by twiddles[0], which is 1, by rotating the loop rather than duplicating the body. In fixed point twiddles[0] is 32767 rather than 32768, so skipping it also drops a small systematic gain error. Then celt/arm/kiss_fft_armv4_asm.S replaces the radix-3, radix-4 and radix-5 bodies, reached through OVERRIDE_kf_bfly3/4/5. The compiled kernels spill their loop-invariant pointers and reload them every pass, and gcc will not form ldm/stm from contiguous C accesses: it reorders the loads while scheduling and does not hand out ascending register pairs. The assembly keeps the bookkeeping resident and sends the transient butterfly values to the frame instead, in bursts. Over 160 ms of stereo music, traced under qemu and costed with an ARM7TDMI model: FFT cycles 1,876,926 -> 1,430,748 -23.8% whole decode 8,142,054 -> 7,695,876 -5.5% loads 769,935 -> 650,157 -15.6% stores 409,999 -> 350,605 -14.5% multiplies 350,909 -> 337,493 -3.8% text 3,368 -> 2,656 bytes The radix-5 fold accounts for the whole multiply reduction. The assembly leaves the count untouched and wins purely on memory traffic. Accuracy improves by up to 2.9 dB rather than degrading, because the assembly keeps all 32 bits of each Q15 product where MULT16_32_Q15_armv4 drops the low bit. Decoded output differs from the C build in 27 of 15,360 samples, each by one LSB. Build with OPUS_ARM_NO_FFT_ASM to select the C butterflies instead. Co-Authored-By: Claude Opus 5 Change-Id: I2e8a0904da8a85b654ff648a01234348977a0edc --- lib/rbcodec/codecs/libopus/SOURCES | 3 + .../codecs/libopus/celt/_kiss_fft_guts.h | 6 + .../codecs/libopus/celt/arm/kiss_fft_armv4.h | 25 + .../libopus/celt/arm/kiss_fft_armv4_asm.S | 609 ++++++++++++++++++ lib/rbcodec/codecs/libopus/celt/kiss_fft.c | 115 +++- 5 files changed, 727 insertions(+), 31 deletions(-) create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv4_asm.S diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index 53806545c7..c984350c5b 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -8,6 +8,9 @@ celt/entcode.c celt/entdec.c celt/entenc.c celt/kiss_fft.c +#if defined(CPU_ARM) && (ARM_ARCH == 4) +celt/arm/kiss_fft_armv4_asm.S +#endif celt/laplace.c celt/mathops.c celt/mdct.c diff --git a/lib/rbcodec/codecs/libopus/celt/_kiss_fft_guts.h b/lib/rbcodec/codecs/libopus/celt/_kiss_fft_guts.h index ea847ff31d..b2d2fdd7f3 100644 --- a/lib/rbcodec/codecs/libopus/celt/_kiss_fft_guts.h +++ b/lib/rbcodec/codecs/libopus/celt/_kiss_fft_guts.h @@ -160,14 +160,20 @@ # define KISS_FFT_COS(phase) floor(.5+TWID_MAX*cos (phase)) # define KISS_FFT_SIN(phase) floor(.5+TWID_MAX*sin (phase)) # define HALF_OF(x) ((x)>>1) +/* Unlike HALF_OF, this rounds rather than truncating. It stands in for a + multiply in kf_bfly5, where a floor bias costs measurable accuracy and the + extra add is far cheaper than the multiply it replaces. */ +# define QUARTER_OF(x) (((x)+2)>>2) #elif defined(USE_SIMD) # define KISS_FFT_COS(phase) _mm_set1_ps( cos(phase) ) # define KISS_FFT_SIN(phase) _mm_set1_ps( sin(phase) ) # define HALF_OF(x) ((x)*_mm_set1_ps(.5f)) +# define QUARTER_OF(x) ((x)*_mm_set1_ps(.25f)) #else # define KISS_FFT_COS(phase) (kiss_fft_scalar) cos(phase) # define KISS_FFT_SIN(phase) (kiss_fft_scalar) sin(phase) # define HALF_OF(x) ((x)*.5f) +# define QUARTER_OF(x) ((x)*.25f) #endif #define kf_cexp(x,phase) \ diff --git a/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv4.h b/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv4.h index e4faad6f2b..1a82380783 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv4.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv4.h @@ -116,6 +116,31 @@ } \ while(0) +/* Hand-written radix bodies. See celt/arm/kiss_fft_armv4_asm.S for why: + briefly, the compiled kernels spill their loop-invariant pointers and + reload them every pass, and gcc will not form ldm/stm from contiguous C + accesses. The assembly keeps the bookkeeping resident and moves the data + in bursts. These take st->twiddles rather than st, so the assembly needs + no knowledge of the layout of kiss_fft_state. */ +#ifndef OPUS_ARM_NO_FFT_ASM +#define OVERRIDE_kf_bfly3 +#define OVERRIDE_kf_bfly4 +#define OVERRIDE_kf_bfly5 + +void kf_bfly3_armv4(kiss_fft_cpx *Fout, const kiss_twiddle_cpx *tw, + int fstride, int m, int N, int mm); +void kf_bfly4_armv4(kiss_fft_cpx *Fout, const kiss_twiddle_cpx *tw, + int fstride, int m, int N, int mm); +void kf_bfly5_armv4(kiss_fft_cpx *Fout, const kiss_twiddle_cpx *tw, + int fstride, int m, int N, int mm); + +#define kf_bfly3(Fout, fstride, st, m, N, mm) \ + kf_bfly3_armv4((Fout), (st)->twiddles, (int)(fstride), (m), (N), (mm)) +#define kf_bfly4(Fout, fstride, st, m, N, mm) \ + kf_bfly4_armv4((Fout), (st)->twiddles, (int)(fstride), (m), (N), (mm)) +#define kf_bfly5(Fout, fstride, st, m, N, mm) kf_bfly5_armv4((Fout), (st)->twiddles, (int)(fstride), (m), (N), (mm)) +#endif /* OPUS_ARM_NO_FFT_ASM */ + #endif /* FIXED_POINT */ #endif /* KISS_FFT_ARMv4_H */ diff --git a/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv4_asm.S new file mode 100644 index 0000000000..50a037271a --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv4_asm.S @@ -0,0 +1,609 @@ +/* ARMv4 FFT butterflies for the CELT fixed-point kiss_fft. + * + * Copyright (c) 2003-2004, Mark Borgerding + * Copyright (c) 2005-2007, Xiph.Org Foundation + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/kiss_fft.c are met. + * + * Why this exists + * --------------- + * On ARM7TDMI a single load costs 1S+1N+1I = 3 cycles and a store 2N = 2, + * while a load multiple of n registers costs n+2 and a store multiple n+1. + * Every extra word moved in a burst is two cycles cheaper to load and one + * cheaper to store, and on the PP502x, where instructions and data share one + * cache, a burst arbitrates against instruction fetch once instead of n + * times. + * + * gcc cannot be steered into that from C. It reorders the accesses while + * scheduling and does not hand out ascending register pairs, so contiguous + * loads come out as separate ldr. Worse, the radix kernels need more live + * values than ARM has registers, and gcc resolves that by spilling the + * loop-invariant pointers and strides and reloading them every pass: the + * compiled radix-4 body reloads eleven words from its frame per iteration. + * + * These routines invert that. Every pointer, twiddle pointer and counter + * stays in a register for the whole inner loop, and the transient butterfly + * values are what goes to the frame, moved in ldm/stm bursts. The spill is + * genuinely unavoidable -- a radix-4 pass needs four transformed points plus + * a complex multiply in flight, which is more than fourteen words -- but + * here it is placed rather than accidental. + * + * Complex multiply register discipline follows the C_MUL asm already in + * kiss_fft_armv4.h: the twiddle stays a 16-bit value in Rs, and the Q15 + * narrowing keeps all 32 bits of the product. MULT16_32_Q15_armv4, used by + * the C C_MULBYSCALAR, drops the low bit for speed, so the radix-3 twiddle + * by epi3 comes out one LSB more accurate here than in the C path. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "kiss_fft_armv4_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 + +/* ------------------------------------------------------------------------ + * void kf_bfly4_armv4(kiss_fft_cpx *Fout, const kiss_twiddle_cpx *tw, + * int fstride, int m, int N, int mm) + * + * r0 = Fout r1 = tw r2 = fstride r3 = m [sp+0] = N [sp+4] = mm + * ------------------------------------------------------------------------ */ + .global kf_bfly4_armv4 + .type kf_bfly4_armv4, %function +kf_bfly4_armv4: + push {r4-r11, lr} + cmp r3, #1 + bne .Lb4_general + +/* Degenerate case: every twiddle is 1, so four consecutive complex points -- + eight consecutive words -- go in and eight come out with no multiplies at + all. This is the one kernel that fits entirely in registers, so it is one + ldm of eight and one stm of eight per group. */ + ldr r1, [sp, #36] @ N +.Lb4_m1: + ldm r0, {r4, r5, r6, r7, r8, r9, r10, r11} + sub ip, r4, r8 @ s0.r = F0.r - F2.r + sub lr, r5, r9 @ s0.i + add r4, r4, r8 @ F0.r += F2.r + add r5, r5, r9 + add r8, r6, r10 @ s1.r = F1.r + F3.r + add r9, r7, r11 @ s1.i + sub r6, r6, r10 @ d.r = F1.r - F3.r + sub r7, r7, r11 @ d.i + sub r10, r4, r8 @ F2.r = F0.r - s1.r + sub r11, r5, r9 @ F2.i + add r4, r4, r8 @ F0.r += s1.r + add r5, r5, r9 + add r8, ip, r7 @ F1.r = s0.r + d.i + sub r9, lr, r6 @ F1.i = s0.i - d.r + sub ip, ip, r7 @ F3.r = s0.r - d.i + add lr, lr, r6 @ F3.i = s0.i + d.r + stmia r0!, {r4, r5, r8, r9, r10, r11, ip, lr} + subs r1, r1, #1 + bne .Lb4_m1 + pop {r4-r11, pc} + +/* Frame, 52 bytes. The first six words are the placed spill; everything + from 24 on is outer-loop state, read once per i and never inside the + inner loop. + 0 T1.r 4 T1.i 8 T0.r 12 T0.i 16 D.r 20 D.i + The outer-loop half is ordered to match r0-r6 at the end of the prologue, + so one stm stores it: 24 Fout, 28 tw, 32 fstride, 36 m, 40 N, 44 mm*8, + 48 m*8. */ +.Lb4_general: + sub sp, sp, #52 + ldr r4, [sp, #88] @ N + ldr r5, [sp, #92] @ mm + mov r2, r2, lsl #2 @ fstride in bytes (4 per kiss_twiddle_cpx) + mov r5, r5, lsl #3 @ mm * sizeof(kiss_fft_cpx) + mov r6, r3, lsl #3 @ M = m * sizeof(kiss_fft_cpx) + add ip, sp, #24 + stmia ip, {r0-r6} + +.Lb4_outer: + ldr r0, [sp, #24] @ Fout for this i + ldr r5, [sp, #44] @ mm*8 + add r5, r0, r5 + str r5, [sp, #24] @ Fout for i+1 + ldr r5, [sp, #48] @ M + add r1, r0, r5 @ p1 = Fout + m + add r2, r1, r5 @ p2 = Fout + 2m + add r3, r2, r5 @ p3 = Fout + 3m + ldr r4, [sp, #28] @ tw1 = twiddles + mov r5, r4 @ tw3 = twiddles + ldr r7, [sp, #36] @ inner count = m + +/* Pass 0 twiddles by twiddles[0], which is 1, so it is peeled: no twiddle + load and no multiply. In fixed point twiddles[0] is 32767 rather than + 32768, so skipping it also drops a small systematic gain error. */ + ldm r0, {r8, r9} @ A + ldm r2, {r6, r11} @ C + add r10, r8, r6 @ T0.r = A.r + C.r + sub r8, r8, r6 @ T1.r = A.r - C.r + add ip, r9, r11 @ T0.i + sub r9, r9, r11 @ T1.i + stmia sp, {r8, r9, r10, ip} + ldm r1, {r6, r11} @ B + ldm r3, {r8, r9} @ D + b .Lb4_combine + +.Lb4_inner: + ldr ip, [sp, #32] @ fstride, the one loop-invariant reload + add r4, r4, ip @ tw1 += fstride + add r5, r5, ip, lsl #1 + add r5, r5, ip @ tw3 += 3*fstride + /* tw2 sits halfway between the two. Deriving it keeps a register free; + written as a difference so the address arithmetic cannot overflow. */ + sub ip, r5, r4 + add ip, r4, ip, lsr #1 + + /* C = tw2 * Fout[2m] */ + ldm r2, {r8, r9} + ldrsh r10, [ip] + ldrsh ip, [ip, #2] + smull lr, r11, r9, r10 + smlal lr, r11, r8, ip + mov lr, lr, lsr #15 + orr r11, lr, r11, lsl #17 @ C.i + rsb ip, ip, #0 + smull r10, r6, r8, r10 + smlal r10, r6, r9, ip + mov r10, r10, lsr #15 + orr r6, r10, r6, lsl #17 @ C.r + + ldm r0, {r8, r9} @ A + add r10, r8, r6 @ T0.r + sub r8, r8, r6 @ T1.r + add ip, r9, r11 @ T0.i + sub r9, r9, r11 @ T1.i + stmia sp, {r8, r9, r10, ip} @ park T1, T0 + + /* D = tw3 * Fout[3m] */ + ldm r3, {r8, r9} + ldrsh r10, [r5] + ldrsh ip, [r5, #2] + smull lr, r11, r9, r10 + smlal lr, r11, r8, ip + mov lr, lr, lsr #15 + orr r11, lr, r11, lsl #17 @ D.i + rsb ip, ip, #0 + smull r10, r6, r8, r10 + smlal r10, r6, r9, ip + mov r10, r10, lsr #15 + orr r6, r10, r6, lsl #17 @ D.r + add lr, sp, #16 + stmia lr, {r6, r11} @ park D + + /* B = tw1 * Fout[m] */ + ldm r1, {r8, r9} + ldrsh r10, [r4] + ldrsh ip, [r4, #2] + smull lr, r11, r9, r10 + smlal lr, r11, r8, ip + mov lr, lr, lsr #15 + orr r11, lr, r11, lsl #17 @ B.i + rsb ip, ip, #0 + smull r10, r6, r8, r10 + smlal r10, r6, r9, ip + mov r10, r10, lsr #15 + orr r6, r10, r6, lsl #17 @ B.r + add lr, sp, #16 + ldm lr, {r8, r9} @ D back + +/* Common tail: B in (r6, r11), D in (r8, r9), T0 and T1 on the frame. */ +.Lb4_combine: + add r10, r6, r8 @ T2.r = B.r + D.r + sub r6, r6, r8 @ T3.r = B.r - D.r + add ip, r11, r9 @ T2.i + sub r11, r11, r9 @ T3.i + ldr r8, [sp, #8] @ T0.r + ldr r9, [sp, #12] @ T0.i + add r8, r8, r10 @ out0.r = T0.r + T2.r + add r9, r9, ip @ out0.i + sub r10, r8, r10, lsl #1 @ out2.r = out0.r - 2*T2.r + sub ip, r9, ip, lsl #1 @ out2.i + stm r0, {r8, r9} + stm r2, {r10, ip} + ldm sp, {r8, r9} @ T1 + add r10, r8, r11 @ out1.r = T1.r + T3.i + sub ip, r9, r6 @ out1.i = T1.i - T3.r + stm r1, {r10, ip} + sub r10, r8, r11 @ out3.r = T1.r - T3.i + add ip, r9, r6 @ out3.i = T1.i + T3.r + stm r3, {r10, ip} + + add r0, r0, #8 + add r1, r1, #8 + add r2, r2, #8 + add r3, r3, #8 + subs r7, r7, #1 + bne .Lb4_inner + + ldr r5, [sp, #40] @ N + subs r5, r5, #1 + str r5, [sp, #40] + bne .Lb4_outer + add sp, sp, #52 + pop {r4-r11, pc} + .size kf_bfly4_armv4, .-kf_bfly4_armv4 + + +/* ------------------------------------------------------------------------ + * void kf_bfly3_armv4(kiss_fft_cpx *Fout, const kiss_twiddle_cpx *tw, + * int fstride, int m, int N, int mm) + * + * Frame, 36 bytes. The outer-loop half is ordered to match r0-r6 at the end + * of the prologue, so one stm stores it: + * 0 s1.r 4 s1.i 8 Fout 12 tw 16 fstride 20 m 24 N + * 28 mm*8 32 m*8 + * ------------------------------------------------------------------------ */ + .align 2 + .global kf_bfly3_armv4 + .type kf_bfly3_armv4, %function +kf_bfly3_armv4: + push {r4-r11, lr} + sub sp, sp, #36 + ldr r4, [sp, #72] @ N + ldr r5, [sp, #76] @ mm + mov r2, r2, lsl #2 @ fstride in bytes + mov r5, r5, lsl #3 + mov r6, r3, lsl #3 @ M + add ip, sp, #8 + stmia ip, {r0-r6} + ldr r7, .Lb3_epi3 + +.Lb3_outer: + ldr r0, [sp, #8] @ Fout for this i + ldr r4, [sp, #28] @ mm*8 + add r4, r0, r4 + str r4, [sp, #8] + ldr r4, [sp, #32] @ M + add r1, r0, r4 @ p1 = Fout + m + add r2, r1, r4 @ p2 = Fout + 2m + ldr r3, [sp, #12] @ tw1 = twiddles + mov r4, r3 @ tw2 = twiddles + ldr r6, [sp, #20] @ inner count = m + + /* Pass 0: twiddles[0] is 1, so it is peeled. */ + ldm r1, {r8, r9} @ s1 = Fout[m] + ldm r2, {r5, r11} @ s2 = Fout[2m] + b .Lb3_combine + +.Lb3_inner: + ldr ip, [sp, #16] @ fstride + add r3, r3, ip @ tw1 += fstride + add r4, r4, ip, lsl #1 @ tw2 += 2*fstride + + /* s1 = tw1 * Fout[m] */ + ldm r1, {r8, r9} + ldrsh r10, [r3] + ldrsh ip, [r3, #2] + smull lr, r11, r9, r10 + smlal lr, r11, r8, ip + mov lr, lr, lsr #15 + orr r11, lr, r11, lsl #17 @ s1.i + rsb ip, ip, #0 + smull r10, r5, r8, r10 + smlal r10, r5, r9, ip + mov r10, r10, lsr #15 + orr r5, r10, r5, lsl #17 @ s1.r + stmia sp, {r5, r11} @ park s1 + + /* s2 = tw2 * Fout[2m] */ + ldm r2, {r8, r9} + ldrsh r10, [r4] + ldrsh ip, [r4, #2] + smull lr, r11, r9, r10 + smlal lr, r11, r8, ip + mov lr, lr, lsr #15 + orr r11, lr, r11, lsl #17 @ s2.i + rsb ip, ip, #0 + smull r10, r5, r8, r10 + smlal r10, r5, r9, ip + mov r10, r10, lsr #15 + orr r5, r10, r5, lsl #17 @ s2.r + ldm sp, {r8, r9} @ s1 back + +/* Common tail: s1 in (r8, r9), s2 in (r5, r11). */ +.Lb3_combine: + add r10, r8, r5 @ s3.r = s1.r + s2.r + sub r8, r8, r5 @ s0.r = s1.r - s2.r + add ip, r9, r11 @ s3.i + sub r9, r9, r11 @ s0.i + + smull r5, r11, r8, r7 @ s0.r *= epi3.i + mov r5, r5, lsr #15 + orr r8, r5, r11, lsl #17 + smull r5, r11, r9, r7 @ s0.i *= epi3.i + mov r5, r5, lsr #15 + orr r9, r5, r11, lsl #17 + + ldm r0, {r5, r11} @ Fout[0] + add r5, r5, r10 @ Fout[0] += s3 + add r11, r11, ip + stm r0, {r5, r11} + /* Recover the pre-add value and take Fout[0] - s3/2 from it. Cheaper + than holding the original in a register we do not have. */ + sub r5, r5, r10 + sub r5, r5, r10, asr #1 + sub r11, r11, ip + sub r11, r11, ip, asr #1 + + add r10, r5, r9 @ Fout[2m].r = t.r + s0.i + sub ip, r11, r8 @ Fout[2m].i = t.i - s0.r + stm r2, {r10, ip} + sub r10, r5, r9 @ Fout[m].r = t.r - s0.i + add ip, r11, r8 @ Fout[m].i = t.i + s0.r + stm r1, {r10, ip} + + add r0, r0, #8 + add r1, r1, #8 + add r2, r2, #8 + subs r6, r6, #1 + bne .Lb3_inner + + ldr r4, [sp, #24] @ N + subs r4, r4, #1 + str r4, [sp, #24] + bne .Lb3_outer + add sp, sp, #36 + pop {r4-r11, pc} +.Lb3_epi3: + .word -28378 + .size kf_bfly3_armv4, .-kf_bfly3_armv4 + +/* ------------------------------------------------------------------------ + * void kf_bfly5_armv4(kiss_fft_cpx *Fout, const kiss_twiddle_cpx *tw, + * int fstride, int m, int N, int mm) + * + * The radix-5 body needs five points plus a complex multiply live at once, + * which no register file this size can hold, so the four transformed points + * are staged through the frame in ldm/stm pairs. What stays resident is all + * the bookkeeping: the output pointer, the point stride, the twiddle base + * and offset, and the counter. Addresses for the other four points and + * their twiddles are derived from those with shifted adds rather than + * carried in registers, trading one cycle each for four registers. + * + * Frame, 68 bytes: + * 0 s7.r 4 s7.i 8 s10.r 12 s10.i 16 s9.r 20 s9.i + * 32 s5.r 36 s5.i 40 s11.r 44 s11.i + * 48 Fout 52 m 56 N 60 mm*8 64 fstride + * The outer-loop half is ordered so r0, r3, r5, r6 and r7 store with one stm. + * ------------------------------------------------------------------------ */ + .align 2 + .global kf_bfly5_armv4 + .type kf_bfly5_armv4, %function + +/* One complex multiply. Data in r6 (real) and r7 (imag), twiddle real in r8 + and imag in ip, lr as the 64-bit low word. Results land in \mr and \mi. + The register choices satisfy the ARMv4 rule that RdHi, RdLo and Rm differ; + the second smull deliberately reuses Rs as RdLo, exactly as the C_MUL asm + in kiss_fft_armv4.h does. */ + .macro CMUL5 mr, mi + smull lr, \mi, r7, r8 + smlal lr, \mi, r6, ip + mov lr, lr, lsr #15 + orr \mi, lr, \mi, lsl #17 + rsb ip, ip, #0 + smull r8, \mr, r6, r8 + smlal r8, \mr, r7, ip + mov r8, r8, lsr #15 + orr \mr, r8, \mr, lsl #17 + .endm + +kf_bfly5_armv4: + push {r4-r11, lr} + sub sp, sp, #68 + ldr r5, [sp, #104] @ N + ldr r6, [sp, #108] @ mm + mov r7, r2, lsl #2 @ fstride in bytes + mov r2, r1 @ twiddle base + mov r1, r3, lsl #3 @ M = m * sizeof(kiss_fft_cpx) + mov r6, r6, lsl #3 + add ip, sp, #48 + stmia ip, {r0, r3, r5, r6, r7} + +.Lb5_outer: + ldr r0, [sp, #48] @ Fout for this i + ldr r6, [sp, #60] @ mm*8 + add r6, r0, r6 + str r6, [sp, #48] @ Fout for i+1 + ldr r5, [sp, #52] @ inner count = m + mov r3, #0 @ twiddle offset + + /* Pass 0: every twiddle is 1, so the four points are used as they are. */ + add ip, r0, r1 + ldm ip, {r4, r11} @ s1 + add ip, r0, r1, lsl #2 + ldm ip, {r9, r10} @ s4 + add r6, r4, r9 @ s7.r = s1.r + s4.r + add r8, r11, r10 @ s7.i + sub r4, r4, r9 @ s10.r = s1.r - s4.r + sub r11, r11, r10 @ s10.i + stmia sp, {r6, r8} + add ip, sp, #8 + stmia ip, {r4, r11} + add ip, r0, r1, lsl #1 + ldm ip, {r4, r11} @ s2 + add ip, r0, r1 + add ip, ip, r1, lsl #1 + ldm ip, {r9, r10} @ s3 + add r6, r4, r9 @ s8.r = s2.r + s3.r + add r8, r11, r10 @ s8.i + sub r4, r4, r9 @ s9.r = s2.r - s3.r + sub r11, r11, r10 @ s9.i + add ip, sp, #16 + stmia ip, {r4, r11} + b .Lb5_combine + +.Lb5_inner: + ldr ip, [sp, #64] @ fstride, the one loop-invariant reload + add r3, r3, ip @ twiddle offset for this pass + + /* s1 = tw[u] * Fout[m] */ + add ip, r0, r1 + ldm ip, {r6, r7} + add ip, r2, r3 + ldrsh r8, [ip] + ldrsh ip, [ip, #2] + CMUL5 r4, r11 + + /* s4 = tw[4u] * Fout[4m] */ + add ip, r0, r1, lsl #2 + ldm ip, {r6, r7} + add ip, r2, r3, lsl #2 + ldrsh r8, [ip] + ldrsh ip, [ip, #2] + CMUL5 r9, r10 + + add r6, r4, r9 @ s7.r + add r8, r11, r10 @ s7.i + sub r4, r4, r9 @ s10.r + sub r11, r11, r10 @ s10.i + stmia sp, {r6, r8} + add ip, sp, #8 + stmia ip, {r4, r11} + + /* s2 = tw[2u] * Fout[2m] */ + add ip, r0, r1, lsl #1 + ldm ip, {r6, r7} + add ip, r2, r3, lsl #1 + ldrsh r8, [ip] + ldrsh ip, [ip, #2] + CMUL5 r4, r11 + + /* s3 = tw[3u] * Fout[3m] */ + add ip, r0, r1 + add ip, ip, r1, lsl #1 + ldm ip, {r6, r7} + add ip, r2, r3 + add ip, ip, r3, lsl #1 + ldrsh r8, [ip] + ldrsh ip, [ip, #2] + CMUL5 r9, r10 + + add r6, r4, r9 @ s8.r + add r8, r11, r10 @ s8.i + sub r4, r4, r9 @ s9.r + sub r11, r11, r10 @ s9.i + add ip, sp, #16 + stmia ip, {r4, r11} + +/* Common tail: s8 in (r6, r8); s7, s10 and s9 on the frame. */ +.Lb5_combine: + ldm sp, {r9, r10} @ s7 + add r4, r9, r6 @ sum = s7 + s8 + add r11, r10, r8 + sub r9, r9, r6 @ diff = s7 - s8 + sub r10, r10, r8 + + ldm r0, {r6, r7} @ Fout[0] + add r6, r6, r4 @ Fout[0] += sum + add r7, r7, r11 + stm r0, {r6, r7} + /* Recover the pre-add value; the term common to both output pairs is + Fout[0] - sum/4, and QUARTER_OF rounds rather than truncating. */ + sub r6, r6, r4 + sub r7, r7, r11 + add r4, r4, #2 + add r11, r11, #2 + sub r6, r6, r4, asr #2 @ t.r + sub r7, r7, r11, asr #2 @ t.i + + /* diff *= yc, where yc = sqrt(5)/4 absorbs the four cosine products. */ + ldr ip, .Lb5_yc + smull r4, r8, r9, ip + mov r4, r4, lsr #15 + orr r9, r4, r8, lsl #17 + smull r4, r8, r10, ip + mov r4, r4, lsr #15 + orr r10, r4, r8, lsl #17 + + add r4, r6, r9 @ s5 = t + diff*yc + add r8, r7, r10 + sub r6, r6, r9 @ s11 = t - diff*yc + sub r7, r7, r10 + add ip, sp, #32 + stmia ip, {r4, r8} + add ip, sp, #40 + stmia ip, {r6, r7} + + /* The sine terms. Each output component is a pair of products over the + same operands, so they accumulate into one smull/smlal instead of + narrowing twice. */ + add ip, sp, #8 + ldm ip, {r6, r7} @ s10 + add ip, sp, #16 + ldm ip, {r8, r9} @ s9 + ldr r10, .Lb5_yai + ldr r11, .Lb5_ybi + + smull lr, ip, r7, r10 @ s6.r = s10.i*ya.i + s9.i*yb.i + smlal lr, ip, r9, r11 + mov lr, lr, lsr #15 + orr r4, lr, ip, lsl #17 + + rsb r11, r11, #0 + smull lr, ip, r9, r10 @ s12.r = s9.i*ya.i - s10.i*yb.i + smlal lr, ip, r7, r11 + mov lr, lr, lsr #15 + orr r7, lr, ip, lsl #17 + rsb r11, r11, #0 + + smull lr, ip, r6, r10 @ s6.i = -(s10.r*ya.i + s9.r*yb.i) + smlal lr, ip, r8, r11 + mov lr, lr, lsr #15 + orr ip, lr, ip, lsl #17 + rsb r9, ip, #0 + + rsb r10, r10, #0 + smull lr, ip, r6, r11 @ s12.i = s10.r*yb.i - s9.r*ya.i + smlal lr, ip, r8, r10 + mov lr, lr, lsr #15 + orr r10, lr, ip, lsl #17 + + add ip, sp, #32 + ldm ip, {r6, r8} @ s5 + sub r11, r6, r4 @ Fout[m] = s5 - s6 + sub ip, r8, r9 + add r4, r6, r4 @ Fout[4m] = s5 + s6 + add r9, r8, r9 + add lr, r0, r1 + stmia lr, {r11, ip} + add lr, r0, r1, lsl #2 + stmia lr, {r4, r9} + + add ip, sp, #40 + ldm ip, {r6, r8} @ s11 + add r11, r6, r7 @ Fout[2m] = s11 + s12 + add ip, r8, r10 + sub r7, r6, r7 @ Fout[3m] = s11 - s12 + sub r10, r8, r10 + add lr, r0, r1, lsl #1 + stmia lr, {r11, ip} + add lr, r0, r1 + add lr, lr, r1, lsl #1 + stmia lr, {r7, r10} + + add r0, r0, #8 + subs r5, r5, #1 + bne .Lb5_inner + + ldr r6, [sp, #56] @ N + subs r6, r6, #1 + str r6, [sp, #56] + bne .Lb5_outer + add sp, sp, #68 + pop {r4-r11, pc} +.Lb5_yc: + .word 18318 @ sqrt(5)/4 in Q15 +.Lb5_yai: + .word -31164 @ -sin(2*pi/5) +.Lb5_ybi: + .word -19261 @ -sin(4*pi/5) + .size kf_bfly5_armv4, .-kf_bfly5_armv4 + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/kiss_fft.c b/lib/rbcodec/codecs/libopus/celt/kiss_fft.c index 83775165d8..69e85b981a 100644 --- a/lib/rbcodec/codecs/libopus/celt/kiss_fft.c +++ b/lib/rbcodec/codecs/libopus/celt/kiss_fft.c @@ -101,6 +101,7 @@ static void kf_bfly2( } } +#ifndef OVERRIDE_kf_bfly4 static void kf_bfly4( kiss_fft_cpx * Fout, const size_t fstride, @@ -143,21 +144,23 @@ static void kf_bfly4( { Fout = Fout_beg + i*mm; tw3 = tw2 = tw1 = st->twiddles; + /* The first pass twiddles by twiddles[0], which is 1, so it needs no + multiplies at all. Rather than duplicate the body, the loop is + rotated: pass j does its own arithmetic and then twiddles for pass + j+1. In fixed point twiddles[0] is 32767 rather than 32768, so + skipping it also drops a small systematic gain error. */ + scratch[0] = Fout[m]; + scratch[1] = Fout[m2]; + scratch[2] = Fout[m3]; + j = 0; /* m is guaranteed to be a multiple of 4. */ - for (j=0;j= m) + break; + tw1 += fstride; + tw2 += fstride*2; + tw3 += fstride*3; + C_MUL(scratch[0],Fout[m] , *tw1 ); + C_MUL(scratch[1],Fout[m2] , *tw2 ); + C_MUL(scratch[2],Fout[m3] , *tw3 ); } } } } +#endif /* OVERRIDE_kf_bfly4 */ #ifndef RADIX_TWO_ONLY +#ifndef OVERRIDE_kf_bfly3 static void kf_bfly3( kiss_fft_cpx * Fout, const size_t fstride, @@ -200,17 +214,16 @@ static void kf_bfly3( { Fout = Fout_beg + i*mm; tw1=tw2=st->twiddles; + /* twiddles[0] is 1, so the first pass needs no multiplies. The loop is + rotated instead of duplicated: each pass twiddles for the next one. */ + scratch[1] = Fout[m]; + scratch[2] = Fout[m2]; /* For non-custom modes, m is guaranteed to be a multiple of 4. */ k=m; - do { - - C_MUL(scratch[1],Fout[m] , *tw1); - C_MUL(scratch[2],Fout[m2] , *tw2); + for (;;) { C_ADD(scratch[3],scratch[1],scratch[2]); C_SUB(scratch[0],scratch[1],scratch[2]); - tw1 += fstride; - tw2 += fstride*2; Fout[m].r = SUB32_ovflw(Fout->r, HALF_OF(scratch[3].r)); Fout[m].i = SUB32_ovflw(Fout->i, HALF_OF(scratch[3].i)); @@ -226,9 +239,17 @@ static void kf_bfly3( Fout[m].i = ADD32_ovflw(Fout[m].i, scratch[0].r); ++Fout; - } while(--k); + + if (--k == 0) + break; + tw1 += fstride; + tw2 += fstride*2; + C_MUL(scratch[1],Fout[m] , *tw1); + C_MUL(scratch[2],Fout[m2] , *tw2); + } } } +#endif /* OVERRIDE_kf_bfly3 */ #ifndef OVERRIDE_kf_bfly5 @@ -244,20 +265,27 @@ static void kf_bfly5( kiss_fft_cpx *Fout0,*Fout1,*Fout2,*Fout3,*Fout4; int i, u; kiss_fft_cpx scratch[13]; - const kiss_twiddle_cpx *tw; + const kiss_twiddle_cpx *tw1,*tw2,*tw3,*tw4; kiss_twiddle_cpx ya,yb; + /* cos(2*pi/5) + cos(4*pi/5) is exactly -1/2 and cos(2*pi/5) - cos(4*pi/5) + is exactly sqrt(5)/2, so the four cosine products below collapse into a + shift by two plus a single multiply by yc = sqrt(5)/4. The Q15 constants + satisfy the first identity exactly (10126 - 26510 == -16384), so yc is + just (ya.r - yb.r)/2 and no accuracy is given up. */ + opus_val16 yc; kiss_fft_cpx * Fout_beg = Fout; #ifdef FIXED_POINT - ya.r = 10126; + /*ya.r = 10126;*/ /* Folded into yc */ ya.i = -31164; - yb.r = -26510; + /*yb.r = -26510;*/ /* Folded into yc */ yb.i = -19261; + yc = 18318; #else ya = st->twiddles[fstride*m]; yb = st->twiddles[fstride*2*m]; + yc = HALF_OF(ya.r - yb.r); #endif - tw=st->twiddles; for (i=0;itwiddles; + /* twiddles[0] is 1, so the first pass needs no multiplies. The loop is + rotated instead of duplicated: each pass twiddles for the next one. */ + scratch[1] = *Fout1; + scratch[2] = *Fout2; + scratch[3] = *Fout3; + scratch[4] = *Fout4; + u = 0; /* For non-custom modes, m is guaranteed to be a multiple of 4. */ - for ( u=0; ur = ADD32_ovflw(Fout0->r, ADD32_ovflw(scratch[7].r, scratch[8].r)); - Fout0->i = ADD32_ovflw(Fout0->i, ADD32_ovflw(scratch[7].i, scratch[8].i)); + C_ADD( scratch[3],scratch[7],scratch[8]); + C_SUB( scratch[4],scratch[7],scratch[8]); - scratch[5].r = ADD32_ovflw(scratch[0].r, ADD32_ovflw(S_MUL(scratch[7].r,ya.r), S_MUL(scratch[8].r,yb.r))); - scratch[5].i = ADD32_ovflw(scratch[0].i, ADD32_ovflw(S_MUL(scratch[7].i,ya.r), S_MUL(scratch[8].i,yb.r))); + C_ADDTO(*Fout0, scratch[3]); + + /* scratch[0] - (scratch[7]+scratch[8])/4 is the term common to both + output pairs; +/- sqrt(5)/4*(scratch[7]-scratch[8]) separates them. + QUARTER_OF() rounds, which recovers about half the accuracy the + substitution would otherwise cost. */ + scratch[3].r = SUB32_ovflw(scratch[0].r, QUARTER_OF(scratch[3].r)); + scratch[3].i = SUB32_ovflw(scratch[0].i, QUARTER_OF(scratch[3].i)); + scratch[4].r = S_MUL(scratch[4].r,yc); + scratch[4].i = S_MUL(scratch[4].i,yc); + + C_ADD( scratch[5],scratch[3],scratch[4]); + C_SUB( scratch[11],scratch[3],scratch[4]); scratch[6].r = ADD32_ovflw(S_MUL(scratch[10].i,ya.i), S_MUL(scratch[9].i,yb.i)); scratch[6].i = NEG32_ovflw(ADD32_ovflw(S_MUL(scratch[10].r,ya.i), S_MUL(scratch[9].r,yb.i))); @@ -294,8 +338,6 @@ static void kf_bfly5( C_SUB(*Fout1,scratch[5],scratch[6]); C_ADD(*Fout4,scratch[5],scratch[6]); - scratch[11].r = ADD32_ovflw(scratch[0].r, ADD32_ovflw(S_MUL(scratch[7].r,yb.r), S_MUL(scratch[8].r,ya.r))); - scratch[11].i = ADD32_ovflw(scratch[0].i, ADD32_ovflw(S_MUL(scratch[7].i,yb.r), S_MUL(scratch[8].i,ya.r))); scratch[12].r = SUB32_ovflw(S_MUL(scratch[9].i,ya.i), S_MUL(scratch[10].i,yb.i)); scratch[12].i = SUB32_ovflw(S_MUL(scratch[10].r,yb.i), S_MUL(scratch[9].r,ya.i)); @@ -303,6 +345,17 @@ static void kf_bfly5( C_SUB(*Fout3,scratch[11],scratch[12]); ++Fout0;++Fout1;++Fout2;++Fout3;++Fout4; + + if (++u >= m) + break; + tw1 += fstride; + tw2 += fstride*2; + tw3 += fstride*3; + tw4 += fstride*4; + C_MUL(scratch[1] ,*Fout1, *tw1); + C_MUL(scratch[2] ,*Fout2, *tw2); + C_MUL(scratch[3] ,*Fout3, *tw3); + C_MUL(scratch[4] ,*Fout4, *tw4); } } } From d514ee928249c7c07f5738ec612a7a9c8cc16277 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 11 Sep 2026 22:04:48 -0400 Subject: [PATCH 201/408] opus: ARMv4 assembly for the backward MDCT inner loops Cuts realtime decode on the Sansa e200v1 from 52.1 MHz to 50.8 MHz. clt_mdct_backward was the largest remaining item at 13.5% of decode. Only the three inner loops move to assembly. The setup stays in C, so mdct.c remains readable and the assembly needs no knowledge of mdct_lookup. What the compiled loops lose is registers. Each needs more live values than gcc can hold, so it spills the loop-invariant pointers, strides and limits and reloads them every pass: five stack accesses per iteration in the post-rotation alone. Holding the twiddle as a 16-bit value and accumulating the product pair with smull/smlal is what makes the bookkeeping fit, needing seven live registers where the shifted MULT16_32_Q15 form needs nine. ldm/stm helps only where the addressing allows. The post-rotation walks the buffer from both ends and so reads and writes contiguous pairs. The pre-rotation reads the spectrum through a runtime stride and writes through the bitrev table, so only its 8-byte output pair merges, and the TDAC mirror merges nothing. Over 160 ms of stereo music, traced under qemu: clt_mdct_backward 1,037,962 -> 900,982 -13.2% whole decode 7,695,876 -> 7,558,896 -1.8% loads 650,157 -> 611,667 -5.9% stores 350,605 -> 323,605 -7.7% multiplies 337,493 -> 337,493 unchanged Accuracy improves substantially, because all three loops keep 32 bits of each Q15 product where MULT16_32_Q15_armv4 drops the low bit, and the backward MDCT applies three such rounds per sample. The rounding SNR of the backward transform rises about 9.5 dB, and its worst case error falls from 708 to 186. Decoded output differs from the previous build in 90 of 15,360 samples, each by one LSB. Build with OPUS_ARM_NO_MDCT_ASM to select the C loops instead. Co-Authored-By: Claude Opus 5 Change-Id: I3c4404b4dbe581d8bcf1f357266a658a068fdb50 --- lib/rbcodec/codecs/libopus/SOURCES | 1 + .../codecs/libopus/celt/arm/mdct_armv4.h | 60 ++++++ .../codecs/libopus/celt/arm/mdct_armv4_asm.S | 187 ++++++++++++++++++ lib/rbcodec/codecs/libopus/celt/mdct.c | 16 ++ 4 files changed, 264 insertions(+) create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4_asm.S diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index c984350c5b..23c015630b 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -10,6 +10,7 @@ celt/entenc.c celt/kiss_fft.c #if defined(CPU_ARM) && (ARM_ARCH == 4) celt/arm/kiss_fft_armv4_asm.S +celt/arm/mdct_armv4_asm.S #endif celt/laplace.c celt/mathops.c diff --git a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h new file mode 100644 index 0000000000..3b39785685 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h @@ -0,0 +1,60 @@ +/* Copyright (c) 2025 Xiph.Org Foundation and contributors + Copyright (c) 2026 Michael Giacomelli + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef MDCT_ARMv4_H +#define MDCT_ARMv4_H + +/* Hand-written inner loops for the backward MDCT. Only the three loops are + replaced; the setup around them stays in C, so mdct.c remains readable and + the assembly needs no knowledge of mdct_lookup. See + celt/arm/mdct_armv4_asm.S for why the compiled loops lose. + + Building with OPUS_ARM_NO_MDCT_ASM selects the C loops instead, which is + how the two are compared. */ + +#if defined(OPUS_ARM_INLINE_ASM) && defined(FIXED_POINT) \ + && !defined(OPUS_ARM_NO_MDCT_ASM) + +#define OVERRIDE_MDCT_PREROT +#define OVERRIDE_MDCT_POSTROT +#define OVERRIDE_MDCT_MIRROR + +/* step is a byte stride, so the caller scales by sizeof(kiss_fft_scalar). */ +void mdct_prerot_armv4(const kiss_fft_scalar *xp1, + const kiss_fft_scalar *xp2, + const kiss_twiddle_scalar *t, + const opus_int16 *bitrev, + kiss_fft_scalar *yp, int N4, int step); + +void mdct_postrot_armv4(kiss_fft_scalar *yp0, kiss_fft_scalar *yp1, + const kiss_twiddle_scalar *t, int N4, int count); + +void mdct_mirror_armv4(kiss_fft_scalar *xp1, kiss_fft_scalar *yp1, + const opus_val16 *wp1, const opus_val16 *wp2, + int count); + +#endif + +#endif /* MDCT_ARMv4_H */ diff --git a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4_asm.S new file mode 100644 index 0000000000..6612ab653c --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4_asm.S @@ -0,0 +1,187 @@ +/* ARMv4 inner loops for the CELT backward MDCT. + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2008 Xiph.Org Foundation + * Copyright (c) 2008 Gregory Maxwell + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/mdct.c are met. + * + * Why this exists + * --------------- + * The three loops here are the whole cost of clt_mdct_backward, and what the + * compiled versions lose is registers. Each loop needs more live values + * than gcc can hold, so it spills the loop-invariant pointers, strides and + * limits and reloads them every pass: five stack accesses per iteration in + * the post-rotation alone. Here the bookkeeping stays resident, and the + * only reload is the pre-rotation's input stride, which buys the seventh + * working register the complex multiply needs. + * + * Holding the twiddle as a 16-bit value and accumulating the product pair + * with smull/smlal is what makes that fit: seven live registers, where the + * shifted MULT16_32_Q15 form needs nine. The ARM7TDMI datasheet also + * promises a 16-bit Rs will finish in two multiplier passes rather than + * four, but device measurement shows no sign of that paying here. Treat + * the form as register economy, not as a multiplier win. + * + * ldm/stm helps only where the addressing allows it. The post-rotation + * reads and writes contiguous pairs, so it bursts. The pre-rotation reads + * the spectrum through a runtime stride and writes through the bit-reversal + * table, so only its 8-byte output pair merges, and the TDAC mirror walks + * two pointers in opposite directions and merges nothing. + * + * All three keep all 32 bits of each Q15 product, where MULT16_32_Q15_armv4 + * drops the low bit for speed. The backward MDCT applies three such rounds + * per sample, so this is worth about 9.5 dB rather than being incidental. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "mdct_armv4_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 + +/* One Q15 complex multiply, the same register discipline as the C_MUL asm in + arm/kiss_fft_armv4.h. a is (\ar, \ai), b is (\br, \bi), \tt is scratch and + the 64-bit low word. Results: \mr = a.r*b.r - a.i*b.i, \mi = a.r*b.i + + a.i*b.r. \bi and \br are destroyed. \mr must differ from \ar and \br, + \mi from \ai and \tt. */ + .macro CMULQ15 ar, ai, br, bi, tt, mr, mi + smull \tt, \mi, \ai, \br + smlal \tt, \mi, \ar, \bi + mov \tt, \tt, lsr #15 + orr \mi, \tt, \mi, lsl #17 + rsb \bi, \bi, #0 + smull \br, \mr, \ar, \br + smlal \br, \mr, \ai, \bi + mov \br, \br, lsr #15 + orr \mr, \br, \mr, lsl #17 + .endm + + +/* ------------------------------------------------------------------------ + * void mdct_prerot_armv4(const kiss_fft_scalar *xp1, + * const kiss_fft_scalar *xp2, + * const kiss_twiddle_scalar *t, + * const opus_int16 *bitrev, + * kiss_fft_scalar *yp, int N4, int step) + * + * yr = xp2*t[i] + xp1*t[N4+i]; yi = xp1*t[i] - xp2*t[N4+i] + * yp[2*rev+1] = yr; yp[2*rev] = yi; xp1 += step; xp2 -= step + * + * step is in bytes. Both halves of the output are one 8-byte pair, so they + * store together even though the address is scattered by the bitrev table. + * ------------------------------------------------------------------------ */ + .global mdct_prerot_armv4 + .type mdct_prerot_armv4, %function +mdct_prerot_armv4: + push {r4-r11, lr} + sub sp, sp, #12 + ldr r5, [sp, #48] @ yp + ldr r6, [sp, #52] @ N4, also the loop count + ldr r4, [sp, #56] @ step, in bytes + str r4, [sp] @ the one loop-invariant reload + mov r4, r6, lsl #1 @ byte offset from t[i] to t[N4+i] + cmp r6, #0 + ble .Lpre_done +.Lpre_loop: + ldr r7, [sp] + ldr r8, [r0], r7 @ x1 = *xp1++ + ldr r9, [r1], -r7 @ x2 = *xp2-- + ldrsh r11, [r2, r4] @ t[N4+i] + ldrsh r10, [r2], #2 @ t[i] + CMULQ15 r8, r9, r10, r11, lr, r7, ip + ldrsh r10, [r3], #2 @ rev + add r10, r5, r10, lsl #3 + stm r10, {r7, ip} @ yi at 2*rev, yr at 2*rev+1 + subs r6, r6, #1 + bne .Lpre_loop +.Lpre_done: + add sp, sp, #12 + pop {r4-r11, pc} + .size mdct_prerot_armv4, .-mdct_prerot_armv4 + + +/* ------------------------------------------------------------------------ + * void mdct_postrot_armv4(kiss_fft_scalar *yp0, kiss_fft_scalar *yp1, + * const kiss_twiddle_scalar *t, int N4, int count) + * + * Walks the buffer from both ends at once, so each end contributes a + * contiguous complex pair: two ldm in, and the writes pair up as well, apart + * from yp0[0], which is stored early to free a register before the second + * complex multiply. + * + * The read of yp1 must happen before yp1[1] is written, which is why the + * store order here looks less symmetric than the arithmetic. + * ------------------------------------------------------------------------ */ + .align 2 + .global mdct_postrot_armv4 + .type mdct_postrot_armv4, %function +mdct_postrot_armv4: + push {r4-r11, lr} + ldr r5, [sp, #36] @ count + mov r4, r3, lsl #1 @ byte offset from t[k] to t[N4+k] + sub r6, r3, #1 + add r3, r2, r6, lsl #1 @ tb = &t[N4-1], walks down + cmp r5, #0 + ble .Lpost_done +.Lpost_loop: + ldrsh r7, [r2, r4] @ t[N4+i] + ldrsh r6, [r2], #2 @ t[i] + ldm r0, {r8, r9} @ im = yp0[0], re = yp0[1] + /* yr = re*t0 + im*t1, yi = re*t1 - im*t0 */ + CMULQ15 r9, r8, r7, r6, lr, r11, ip + ldm r1, {r8, r9} @ im = yp1[0], re = yp1[1] + str ip, [r0] @ yp0[0] = yr + ldrsh r7, [r3, r4] @ t[N2-i-1] + ldrsh r6, [r3], #-2 @ t[N4-i-1] + /* mr is yi' and mi is yr', so the pair lands with yr' in the lower + register and stores straight into yp1. */ + CMULQ15 r9, r8, r7, r6, lr, ip, r10 + stm r1, {r10, r11} @ yp1[0] = yr', yp1[1] = yi + str ip, [r0, #4] @ yp0[1] = yi' + add r0, r0, #8 + sub r1, r1, #8 + subs r5, r5, #1 + bne .Lpost_loop +.Lpost_done: + pop {r4-r11, pc} + .size mdct_postrot_armv4, .-mdct_postrot_armv4 + + +/* ------------------------------------------------------------------------ + * void mdct_mirror_armv4(kiss_fft_scalar *xp1, kiss_fft_scalar *yp1, + * const opus_val16 *wp1, const opus_val16 *wp2, + * int count) + * + * The TDAC fold. Two data pointers walk in opposite directions and two + * window pointers do the same, so nothing here is contiguous and nothing + * bursts; the win is entirely the multiplier operand form. + * ------------------------------------------------------------------------ */ + .align 2 + .global mdct_mirror_armv4 + .type mdct_mirror_armv4, %function +mdct_mirror_armv4: + push {r4-r11, lr} + ldr r4, [sp, #36] @ count + cmp r4, #0 + ble .Lmir_done +.Lmir_loop: + ldrsh r5, [r3], #-2 @ w2 = *wp2-- + ldrsh r6, [r2], #2 @ w1 = *wp1++ + ldr r7, [r0] @ x1 = *xp1 + ldr r8, [r1] @ x2 = *yp1 + /* lo = w2*x2 - w1*x1, hi = w1*x2 + w2*x1 */ + CMULQ15 r8, r7, r5, r6, lr, r9, ip + str r9, [r1], #4 @ *yp1++ = lo + str ip, [r0], #-4 @ *xp1-- = hi + subs r4, r4, #1 + bne .Lmir_loop +.Lmir_done: + pop {r4-r11, pc} + .size mdct_mirror_armv4, .-mdct_mirror_armv4 + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/mdct.c b/lib/rbcodec/codecs/libopus/celt/mdct.c index 5c6dab5b75..1b13863d59 100644 --- a/lib/rbcodec/codecs/libopus/celt/mdct.c +++ b/lib/rbcodec/codecs/libopus/celt/mdct.c @@ -51,6 +51,9 @@ #include #include "os_support.h" #include "mathops.h" +#if defined(OPUS_ARM_ASM) +#include "arm/mdct_armv4.h" +#endif #include "stack_alloc.h" #if defined(MIPSr1_ASM) @@ -265,6 +268,10 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca kiss_fft_scalar * OPUS_RESTRICT yp = out+(overlap>>1); const kiss_twiddle_scalar * OPUS_RESTRICT t = &trig[0]; const opus_int16 * OPUS_RESTRICT bitrev = l->kfft[shift]->bitrev; +#ifdef OVERRIDE_MDCT_PREROT + mdct_prerot_armv4(xp1, xp2, t, bitrev, yp, N4, + 2*stride*(int)sizeof(kiss_fft_scalar)); +#else for(i=0;ikfft[shift], (kiss_fft_cpx*)(out+(overlap>>1))); @@ -291,6 +299,9 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca const kiss_twiddle_scalar *t = &trig[0]; /* Loop to (N4+1)>>1 to handle odd N4. When N4 is odd, the middle pair will be computed twice. */ +#ifdef OVERRIDE_MDCT_POSTROT + mdct_postrot_armv4(yp0, yp1, t, N4, (N4+1)>>1); +#else for(i=0;i<(N4+1)>>1;i++) { kiss_fft_scalar re, im, yr, yi; @@ -319,6 +330,7 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca yp0 += 2; yp1 -= 2; } +#endif } /* Mirror on both sides for TDAC */ @@ -328,6 +340,9 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca const opus_val16 * OPUS_RESTRICT wp1 = window; const opus_val16 * OPUS_RESTRICT wp2 = window+overlap-1; +#ifdef OVERRIDE_MDCT_MIRROR + mdct_mirror_armv4(xp1, yp1, wp1, wp2, overlap/2); +#else for(i = 0; i < overlap/2; i++) { kiss_fft_scalar x1, x2; @@ -338,6 +353,7 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca wp1++; wp2--; } +#endif } } #endif /* OVERRIDE_clt_mdct_backward */ From cbaf66f372be87b63d95449025f30366aede83ca Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Mon, 21 Sep 2026 21:18:20 +0300 Subject: [PATCH 202/408] rbutilqt/themeeditor: Introduce a new KEYCHAIN_PROFILE variable for Apple notarization If the variable is provided, the resulting .dmg will be uploaded to Apple's servers for notarization. If successful, Apple's notarization ticket is attached to the image. Notarization of ~30MiB .dmg takes 40-50 seconds including upload time, ~30 seconds sans the upload time. Tested building and notarizing of Rockbox Utility, Rockbox Theme Editor, and both as a single target, i.e. deploy. Co-authored-by: ChatGPT-5.6 Luna Change-Id: Ib31b1f825893111d23248fb8511fe5b6c73d5525 --- utils/CMakeLists.txt | 16 ++++++++++++++ utils/cmake/deploy.cmake | 46 ++++++++++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index a778035140..12647bbfba 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -126,6 +126,7 @@ endif() # Setup code signing for macOS if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + # Signing the binaries set(CODESIGN_IDENTITY "-" CACHE STRING "Code signing identity to use for macOS signing") @@ -138,6 +139,21 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") else() message("-- Using Apple code signing identity: ${CODESIGN_IDENTITY}") endif() + + # Submitting the .dmg file to Apple's service to get a + # notarization ticket for distributing outside the App store + set(KEYCHAIN_PROFILE "" CACHE STRING + "Code signing identity to use for macOS signing") + + if(DEFINED ENV{KEYCHAIN_PROFILE}) + set(KEYCHAIN_PROFILE "$ENV{KEYCHAIN_PROFILE}") + endif() + + if("${KEYCHAIN_PROFILE}" STREQUAL "") + message("-- Skipping Apple notarization") + else() + message("-- Using Apple notarization keychain profile: ${KEYCHAIN_PROFILE}") + endif() endif() add_executable(ipodpatcher-bin diff --git a/utils/cmake/deploy.cmake b/utils/cmake/deploy.cmake index 757445c67f..ce4d65b956 100644 --- a/utils/cmake/deploy.cmake +++ b/utils/cmake/deploy.cmake @@ -134,23 +134,51 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") set(_deploy_deps "${_deploy_deps};deploy_${deploy_TARGET}_${_deploy_exe_tgt}") endforeach() + set(_dmg "${CMAKE_BINARY_DIR}/${deploy_TARGET}.dmg") + add_custom_command( # TODO: find a better way to figure the app bundle name. - OUTPUT ${CMAKE_BINARY_DIR}/${deploy_TARGET}.dmg - COMMENT "Running macdeployqt and creating dmg ${deploy_TARGET}" - COMMAND ${MACDEPLOYQT_EXECUTABLE} ${deploy_TARGET}.app + OUTPUT "${_dmg}" + COMMENT "Running macdeployqt and creating ${deploy_TARGET}.dmg" + COMMAND "${MACDEPLOYQT_EXECUTABLE}" "${deploy_TARGET}.app" - COMMAND codesign --force --options runtime --deep --sign "${CODESIGN_IDENTITY}" ${deploy_TARGET}.app + COMMAND codesign --force --options runtime --deep + --sign "${CODESIGN_IDENTITY}" "${deploy_TARGET}.app" - COMMAND ${DMGBUILD} -s ${deploy_DMGBUILDCFG} - -Dappbundle=${deploy_TARGET}.app - ${deploy_TARGET} ${CMAKE_BINARY_DIR}/${deploy_TARGET}.dmg + COMMAND ${DMGBUILD} -s "${deploy_DMGBUILDCFG}" + -Dappbundle="${deploy_TARGET}.app" + "${deploy_TARGET}" "${_dmg}" DEPENDS ${deploy_TARGET} dmgbuild_venv ${_deploy_deps} ) - add_custom_target(deploy_${deploy_TARGET} - DEPENDS ${CMAKE_BINARY_DIR}/${deploy_TARGET}.dmg) + + if("${KEYCHAIN_PROFILE}" STREQUAL "") + add_custom_target(deploy_${deploy_TARGET} + DEPENDS "${_dmg}") + else() + set(_notarized_stamp "${_dmg}.notarized") + + add_custom_command( + OUTPUT "${_notarized_stamp}" + COMMENT "Submitting ${deploy_TARGET}.dmg for notarization" + + COMMAND xcrun notarytool submit "${_dmg}" + --keychain-profile "${KEYCHAIN_PROFILE}" + --wait + + COMMAND xcrun stapler staple "${_dmg}" + + COMMAND "${CMAKE_COMMAND}" -E touch "${_notarized_stamp}" + + DEPENDS "${_dmg}" + VERBATIM + ) + + add_custom_target(deploy_${deploy_TARGET} + DEPENDS "${_notarized_stamp}") + endif() + add_dependencies(deploy deploy_${deploy_TARGET}) endfunction() endif() From be35fdfe1a415f299df8a6ef3e7e60ca62158029 Mon Sep 17 00:00:00 2001 From: Andrew Rice Date: Mon, 21 Sep 2026 14:22:03 +1000 Subject: [PATCH 203/408] docs: add Andrew Rice to CREDITS For the iPod Nano 3G port. Change-Id: I43f169bfc4adfa9fb45b1b9bb4a173d71e5a41e3 --- docs/CREDITS | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CREDITS b/docs/CREDITS index 24b0feb054..ada951d769 100644 --- a/docs/CREDITS +++ b/docs/CREDITS @@ -768,6 +768,7 @@ Hemant Kumar Jerry Shen Olivier Senn David Cormier +Andrew Rice The libmad team The wavpack team From 44e7c009ae401e7080b86a4e5e65156d6f68fb13 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 22 Sep 2026 11:43:00 -0400 Subject: [PATCH 204/408] as3525: Increase bootloader firmware buffer to 2MB Modern rockbox builds are just larger than 1MB on the Sansa Fuze/Fuzev2, which exceeds the reserved space to load the firmware. The other AMS targets are a little under 1MB, so their time is likely to come soon. Bump the buffer to 2MB so we never have to worry about this again. Change-Id: Ie6bf998596f5e55f5cc8e576a2a22639344306a4 --- firmware/target/arm/as3525/boot.lds | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/target/arm/as3525/boot.lds b/firmware/target/arm/as3525/boot.lds index 41737f5112..ac6c25e11f 100644 --- a/firmware/target/arm/as3525/boot.lds +++ b/firmware/target/arm/as3525/boot.lds @@ -7,7 +7,7 @@ OUTPUT_FORMAT(elf32-littlearm) OUTPUT_ARCH(arm) STARTUP(target/arm/crt0.o) -#define LOAD_SIZE 0x100000 /* Reserve 1MB for loading the firmware */ +#define LOAD_SIZE 0x200000 /* Reserve 2MB for loading the firmware */ MEMORY { From 9e7b81f269c5ffe8e732c7d1e891aaac3d7af1fd Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:22 -0400 Subject: [PATCH 205/408] opus: exact bounded divide for the range decoder 98.6% of __udivsi3 calls are ec_decode and ec_decode_bin computing val/ext, where the quotient only matters below 2^16. Exact over 80 million cases including corrupt-stream values. Modelled: -1.34% ARMv4, -2.37% ARMv5E. Measured with the previous commit: e200v1 50.80 -> 49.42 MHz, Clip+ 33.40 -> 32.83 MHz. Co-Authored-By: Claude Opus 5 Change-Id: Ia09cb6cd8e42d4885e4d5be0f84862b9b951136c --- lib/rbcodec/codecs/libopus/celt/entdec.c | 40 ++++++++++++++++++++++-- lib/rbcodec/codecs/libopus/config.h | 6 ++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/lib/rbcodec/codecs/libopus/celt/entdec.c b/lib/rbcodec/codecs/libopus/celt/entdec.c index 0b3433ed8b..f706fa80ff 100644 --- a/lib/rbcodec/codecs/libopus/celt/entdec.c +++ b/lib/rbcodec/codecs/libopus/celt/entdec.c @@ -136,17 +136,53 @@ void ec_dec_init(ec_dec *_this,unsigned char *_buf,opus_uint32 _storage){ ec_dec_normalize(_this); } +#if defined(OPUS_EC_DECODE_DIV) +/*val/ext for ec_decode() and ec_decode_bin(), without a libgcc call. + While applicable to any CPU with slow division, particularly on ARMv4/v5 + libgcc is built without CLZ, so both ARMv4 and ARMv5E use a shift-subtract + __udivsi3 here, and these two sites make 98.6% of the decoder's calls to it. + The quotient is only used as _ft-EC_MINI(s+1,_ft), so it matters only below + _ft, and every Opus caller has _ft<=32768. Since val>(_j))>=_d){_n-=_d<<(_j);s|=1U<<(_j);} + if(_small){ + if((_n>>9)>=_d)return 0xFFFF; + goto from8; + } + if((_n>>16)>=_d)return 0xFFFF; + EC_DIV_STEP(15) EC_DIV_STEP(14) EC_DIV_STEP(13) EC_DIV_STEP(12) + EC_DIV_STEP(11) EC_DIV_STEP(10) EC_DIV_STEP(9) +from8: + EC_DIV_STEP(8) EC_DIV_STEP(7) EC_DIV_STEP(6) EC_DIV_STEP(5) + EC_DIV_STEP(4) EC_DIV_STEP(3) EC_DIV_STEP(2) EC_DIV_STEP(1) + EC_DIV_STEP(0) +#undef EC_DIV_STEP + return s; +} +# define EC_DEC_DIV(_n,_d,_ft) (ec_dec_div16(_n,_d,(_ft)<=256)) +#else +# define EC_DEC_DIV(_n,_d,_ft) ((unsigned)((_n)/(_d))) +#endif + unsigned ec_decode(ec_dec *_this,unsigned _ft){ unsigned s; + celt_sig_assert(_ft<=32768); _this->ext=celt_udiv(_this->rng,_ft); - s=(unsigned)(_this->val/_this->ext); + s=EC_DEC_DIV(_this->val,_this->ext,_ft); return _ft-EC_MINI(s+1,_ft); } unsigned ec_decode_bin(ec_dec *_this,unsigned _bits){ unsigned s; + celt_sig_assert(_bits<=15); _this->ext=_this->rng>>_bits; - s=(unsigned)(_this->val/_this->ext); + s=EC_DEC_DIV(_this->val,_this->ext,1U<<_bits); return (1U<<_bits)-EC_MINI(s+1U,1U<<_bits); } diff --git a/lib/rbcodec/codecs/libopus/config.h b/lib/rbcodec/codecs/libopus/config.h index f9fdfb0ec8..4613659687 100644 --- a/lib/rbcodec/codecs/libopus/config.h +++ b/lib/rbcodec/codecs/libopus/config.h @@ -47,6 +47,12 @@ #elif ARM_ARCH > 4 #define OPUS_ARM_INLINE_EDSP #endif + +/*optimization no hardware division support*/ +#if !defined(ARM_HAVE_HW_DIV) +#define OPUS_EC_DECODE_DIV +#endif + #endif #if defined(CPU_COLDFIRE) From f26c9557c257a8579194462b3b4ed1766d463e8b Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:23 -0400 Subject: [PATCH 206/408] opus: ARM kernels for the 16-bit band loops denormalise_bands on both cores; exp_rotation1, haar1 and the normalise_residual scaling loop on ARMv5E. Bit-exact. Modelled: -0.50% ARMv4, -2.91% ARMv5E. Co-Authored-By: Claude Opus 5 Change-Id: If40f5dbe7cb9e7c12aa0a5b4ac9e73433a850b6e --- lib/rbcodec/codecs/libopus/SOURCES | 6 + .../codecs/libopus/celt/arm/bands_arm.h | 63 +++++++ .../libopus/celt/arm/denorm_armv4_asm.S | 82 +++++++++ .../libopus/celt/arm/denorm_armv5e_asm.S | 90 ++++++++++ .../celt/arm/exp_rotation1_armv5e_asm.S | 163 ++++++++++++++++++ .../libopus/celt/arm/haar1_armv5e_asm.S | 89 ++++++++++ .../libopus/celt/arm/normres_armv5e_asm.S | 81 +++++++++ lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h | 56 ++++++ lib/rbcodec/codecs/libopus/celt/bands.c | 14 ++ lib/rbcodec/codecs/libopus/celt/vq.c | 8 + lib/rbcodec/codecs/libopus/config.h | 5 + 11 files changed, 657 insertions(+) create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/denorm_armv4_asm.S create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/denorm_armv5e_asm.S create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/exp_rotation1_armv5e_asm.S create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/haar1_armv5e_asm.S create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/normres_armv5e_asm.S create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index 23c015630b..fef8a216f1 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -11,6 +11,12 @@ celt/kiss_fft.c #if defined(CPU_ARM) && (ARM_ARCH == 4) celt/arm/kiss_fft_armv4_asm.S celt/arm/mdct_armv4_asm.S +celt/arm/denorm_armv4_asm.S +#elif defined(CPU_ARM) && (ARM_ARCH == 5) +celt/arm/denorm_armv5e_asm.S +celt/arm/exp_rotation1_armv5e_asm.S +celt/arm/haar1_armv5e_asm.S +celt/arm/normres_armv5e_asm.S #endif celt/laplace.c celt/mathops.c diff --git a/lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h new file mode 100644 index 0000000000..4ff0952aa8 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h @@ -0,0 +1,63 @@ +/* Copyright (c) 2025 Xiph.Org Foundation and contributors + Copyright (c) 2026 Michael Giacomelli + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef BANDS_ARM_H +#define BANDS_ARM_H + +/* Hand-written inner loops for celt/bands.c on both ARM generations. See + celt/arm/denorm_armv4_asm.S and denorm_armv5e_asm.S. Both are bit-exact + with the C they replace. + + Build with OPUS_ARM_NO_BANDS_ASM to select the C loops instead, which is + how the two are compared. */ + +#if defined(FIXED_POINT) && !defined(OPUS_ARM_NO_BANDS_ASM) + +# if defined(OPUS_ARM_INLINE_ASM) && (ARM_ARCH == 4) + +# define OVERRIDE_DENORM_BAND +void denorm_band_armv4(celt_sig *f, const celt_norm *x, int n, int g, + int shift); +# define DENORM_BAND denorm_band_armv4 + +# elif defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH == 5) + +# define OVERRIDE_DENORM_BAND +void denorm_band_armv5e(celt_sig *f, const celt_norm *x, int n, int g, + int shift); +# define DENORM_BAND denorm_band_armv5e + +/* ARMv5E only: on ARMv4 haar1 is multiply-bound and gains only the hoisted + rounding add. bands.h has already declared haar1 by this point, so the + macro renames the calls and the guarded C definition drops out. */ +# define OVERRIDE_haar1 +void haar1_armv5e(celt_norm *X, int N0, int stride); +# define haar1(X, N0, stride) haar1_armv5e((X), (N0), (stride)) + +# endif + +#endif + +#endif /* BANDS_ARM_H */ diff --git a/lib/rbcodec/codecs/libopus/celt/arm/denorm_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/denorm_armv4_asm.S new file mode 100644 index 0000000000..7e5ad8ac46 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/denorm_armv4_asm.S @@ -0,0 +1,82 @@ +/* ARMv4 inner loop for denormalise_bands. + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2009 Xiph.Org Foundation + * Copyright (c) 2008-2009 Gregory Maxwell + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/bands.c are met. + * + * Why this exists + * --------------- + * The compiled loop costs about sixteen cycles per element on the ARM7TDMI: + * an ldrsh, a mul, a register-specified shift, a str, and a counter + * increment, compare and taken branch every element. + * + * On this core a packed word load does not pay, because unpacking two signed + * halfwords takes three shifts and costs what the second ldrsh saved. What + * does pay is the rest: the two results leave as one stm, three cycles + * rather than four, and the loop test and taken branch happen once per pair + * rather than once per element. About thirteen and a half cycles per + * element. + * + * Bit-exact: ldrsh gives the same sign-extended operand MULT16_16 casts to, + * the product fits in 32 bits, and the shift is the same arithmetic shift. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "denorm_armv4_asm.S must be assembled in ARM mode" +#endif + +/* These kernels are 2,892 bytes that carry 43% of the cycles an ARMv4 decode + * spends, which makes them the densest thing in the codec to put in IRAM. + * config.h sets OPUS_ARM_ICODE on the targets whose codec IRAM window has + * room; everywhere else this stays in .text. The firmware config.h it reads + * that from is assembly-safe, and libopus/config.h guards the rest of its + * includes with __ASSEMBLER__ so this include costs nothing here. */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + + .text + .align 2 + +/* ------------------------------------------------------------------------ + * void denorm_band_armv4(celt_sig *f, const celt_norm *x, int n, + * int g, int shift) + * + * f[i] = (x[i]*g) >> shift for i in [0,n), with 0 <= shift <= 31. n is at + * least 1 in every call from denormalise_bands; a non-positive n writes + * nothing. mul requires Rd and Rm to differ, so g is Rm throughout. + * ------------------------------------------------------------------------ */ + .global denorm_band_armv4 + .type denorm_band_armv4, %function +denorm_band_armv4: + push {r4, lr} + ldr ip, [sp, #8] @ shift + subs r2, r2, #2 @ r2 = elements left after the next pair + blt .Ldb4_tail +.Ldb4_pair: + ldrsh r4, [r1], #2 + ldrsh lr, [r1], #2 + mul r4, r3, r4 @ x[i] * g + mul lr, r3, lr @ x[i+1] * g + mov r4, r4, asr ip + mov lr, lr, asr ip + stmia r0!, {r4, lr} @ f[i] = r4, f[i+1] = lr + subs r2, r2, #2 + bge .Ldb4_pair +.Ldb4_tail: + adds r2, r2, #2 @ 0 or 1 left, or negative for n <= 0 + ble .Ldb4_done + ldrsh r4, [r1], #2 + mul lr, r3, r4 + mov lr, lr, asr ip + str lr, [r0], #4 +.Ldb4_done: + pop {r4, pc} + .size denorm_band_armv4, .-denorm_band_armv4 + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/arm/denorm_armv5e_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/denorm_armv5e_asm.S new file mode 100644 index 0000000000..9bda4cc162 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/denorm_armv5e_asm.S @@ -0,0 +1,90 @@ +/* ARMv5E inner loop for denormalise_bands. + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2009 Xiph.Org Foundation + * Copyright (c) 2008-2009 Gregory Maxwell + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/bands.c are met. + * + * Why this exists + * --------------- + * The compiled loop is seven instructions that cost twelve cycles per + * element on the ARM926: + * + * ldrh r3, [r4], #2 + * smulbb r3, r3, r1 @ uses r3 at once: a two-cycle stall + * add r0, r0, #1 + * asr r3, r3, r7 @ register-specified shift, two cycles + * cmp ip, r0 + * str r3, [lr], #4 + * bgt .L324 @ three + * + * celt_norm is opus_int16, so two coefficients are one word. One ldr + * fetches both, smulbb and smultb take the halves apart, and the pair of + * results leaves as one stm. A subs between the load and its first use + * keeps the load clear of the stall and doubles as the loop test. That is + * about six and a half cycles per element. + * + * Bit-exact: smulbb and smultb multiply exactly the signed 16-bit operands + * MULT16_16 casts to, and the shift is the same arithmetic shift. + * + * The word load needs x on a word boundary. celt_norm arrays are only + * halfword-aligned, so one scalar element runs first when x is not. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "denorm_armv5e_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 + +/* ------------------------------------------------------------------------ + * void denorm_band_armv5e(celt_sig *f, const celt_norm *x, int n, + * int g, int shift) + * + * f[i] = (x[i]*g) >> shift for i in [0,n), with 0 <= shift <= 31. n is at + * least 1 in every call from denormalise_bands, since band edges strictly + * increase; a non-positive n writes nothing. + * ------------------------------------------------------------------------ */ + .global denorm_band_armv5e + .type denorm_band_armv5e, %function +denorm_band_armv5e: + push {r4, r5, lr} + ldr ip, [sp, #12] @ shift + cmp r2, #0 + ble .Ldb5_done + tst r1, #2 @ one element to reach a word boundary? + beq .Ldb5_aligned + ldrsh r4, [r1], #2 + sub r2, r2, #1 + smulbb r5, r4, r3 + mov r5, r5, asr ip + str r5, [r0], #4 +.Ldb5_aligned: + subs r2, r2, #2 @ r2 = elements left after the next pair + blt .Ldb5_tail +.Ldb5_pair: + ldr r4, [r1], #4 @ x[i] low halfword, x[i+1] high + subs r2, r2, #2 @ loop test, and the gap before r4 is used + smulbb r5, r4, r3 @ x[i] * g + smultb lr, r4, r3 @ x[i+1] * g + mov r5, r5, asr ip + mov lr, lr, asr ip + stmia r0!, {r5, lr} @ f[i] = r5, f[i+1] = lr + bge .Ldb5_pair +.Ldb5_tail: + adds r2, r2, #2 @ 0 or 1 left + beq .Ldb5_done + ldrsh r4, [r1], #2 + smulbb r5, r4, r3 + mov r5, r5, asr ip + str r5, [r0], #4 +.Ldb5_done: + pop {r4, r5, pc} + .size denorm_band_armv5e, .-denorm_band_armv5e + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/arm/exp_rotation1_armv5e_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/exp_rotation1_armv5e_asm.S new file mode 100644 index 0000000000..0a01472f29 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/exp_rotation1_armv5e_asm.S @@ -0,0 +1,163 @@ +/* ARMv5E exp_rotation1 for the CELT spreading rotation. + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2009 Xiph.Org Foundation + * Copyright (c) 2007-2009 Timothy B. Terriberry + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/vq.c are met. + * + * Why this exists + * --------------- + * The compiled loop loads x1 and x2 with ldrsh and hands each straight to + * smulbb, two two-cycle stalls per iteration, 18% of the function on the + * ARM926. Every load here sits at least two instructions ahead of its first + * use. + * + * At stride 1, which is 82 of 148 calls, the loop also reloads the value it + * just wrote: Xptr[stride] becomes the next Xptr[0]. Carrying it instead + * saves one load per iteration, and since nothing reads that slot again + * before the next iteration overwrites it, its store is deferred to the end + * of the loop, saving one store too. + * + * Bit-exact. The carried register holds the full 32-bit shifted value + * rather than the 16-bit value EXTRACT16 would store, but every later use of + * it is the bottom-halfword operand of smulbb or smlabb, which reads exactly + * the bits that would have been stored, and the final strh stores them. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "exp_rotation1_armv5e_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 + +/* ------------------------------------------------------------------------ + * void exp_rotation1_armv5e(celt_norm *X, int len, int stride, + * opus_val16 c, opus_val16 s) + * + * Forward over i = 0 .. len-stride-1, then backward over + * i = len-2*stride-1 .. 0, each step + * x1 = X[i], x2 = X[i+stride] + * X[i+stride] = (c*x2 + s*x1 + 16384) >> 15 + * X[i] = (c*x1 - s*x2 + 16384) >> 15 + * + * r0 X, r1 len, r2 stride, r3 c, r4 s, r5 -s, r6 count, r7 x1, r8 x2, + * r9 and ip the two results, r10 the walking pointer. + * ------------------------------------------------------------------------ */ + .global exp_rotation1_armv5e + .type exp_rotation1_armv5e, %function +exp_rotation1_armv5e: + push {r4-r10, lr} + ldr r4, [sp, #32] @ s + rsb r5, r4, #0 @ ms; its low halfword wraps as NEG16 does + cmp r2, #1 + bne .Ler_general + + /* ---- stride 1, forward: carry X[i+1] into the next x1 ---- */ + subs r6, r1, #1 @ len-1 iterations + ble .Ler_back1 + mov r10, r0 + ldrsh r7, [r10] @ x1 = X[0] +.Ler_f1: + ldrsh r8, [r10, #2] @ x2 = X[i+1] + smulbb r9, r3, r7 @ c*x1 + subs r6, r6, #1 @ the gap before r8 is used + smulbb ip, r3, r8 @ c*x2 + smlabb r9, r5, r8, r9 @ c*x1 - s*x2 + smlabb ip, r4, r7, ip @ c*x2 + s*x1 +#ifndef OPUS_ARM_FAST_ROUND + add r9, r9, #0x4000 +#endif +#ifndef OPUS_ARM_FAST_ROUND + add ip, ip, #0x4000 +#endif + mov r9, r9, asr #15 + mov r7, ip, asr #15 @ X[i+1], carried as the next x1 + strh r9, [r10], #2 @ X[i] + bne .Ler_f1 + strh r7, [r10] @ the last X[i+1] + + /* ---- stride 1, backward: carry X[i] into the next x2 ---- */ +.Ler_back1: + subs r6, r1, #2 @ len-2 iterations + ble .Ler_done + add r10, r0, r6, lsl #1 @ &X[len-2] + ldrsh r8, [r10] @ x2 = X[len-2] +.Ler_b1: + ldrsh r7, [r10, #-2]! @ x1 = X[i], r10 = &X[i] + smulbb ip, r3, r8 @ c*x2 + subs r6, r6, #1 + smulbb r9, r3, r7 @ c*x1 + smlabb ip, r4, r7, ip @ c*x2 + s*x1 + smlabb r9, r5, r8, r9 @ c*x1 - s*x2 +#ifndef OPUS_ARM_FAST_ROUND + add ip, ip, #0x4000 +#endif +#ifndef OPUS_ARM_FAST_ROUND + add r9, r9, #0x4000 +#endif + mov ip, ip, asr #15 + mov r8, r9, asr #15 @ X[i], carried as the next x2 + strh ip, [r10, #2] @ X[i+1] + bne .Ler_b1 + strh r8, [r10] @ the last X[i], which is X[0] + b .Ler_done + + /* ---- any other stride: both loads, both stores ---- */ +.Ler_general: + mov r2, r2, lsl #1 @ stride in bytes, numerically 2*stride + subs r6, r1, r2, lsr #1 @ len-stride iterations + ble .Ler_backg + mov r10, r0 +.Ler_fg: + ldrsh r7, [r10] @ x1 + ldrsh r8, [r10, r2] @ x2 + subs r6, r6, #1 + smulbb r9, r3, r7 + smulbb ip, r3, r8 + smlabb r9, r5, r8, r9 @ c*x1 - s*x2 + smlabb ip, r4, r7, ip @ c*x2 + s*x1 +#ifndef OPUS_ARM_FAST_ROUND + add r9, r9, #0x4000 +#endif +#ifndef OPUS_ARM_FAST_ROUND + add ip, ip, #0x4000 +#endif + mov r9, r9, asr #15 + mov ip, ip, asr #15 + strh ip, [r10, r2] @ X[i+stride] + strh r9, [r10], #2 @ X[i] + bne .Ler_fg +.Ler_backg: + subs r6, r1, r2 @ len-2*stride iterations + ble .Ler_done + sub r10, r6, #1 + add r10, r0, r10, lsl #1 @ &X[len-2*stride-1] +.Ler_bg: + ldrsh r7, [r10] + ldrsh r8, [r10, r2] + subs r6, r6, #1 + smulbb r9, r3, r7 + smulbb ip, r3, r8 + smlabb r9, r5, r8, r9 + smlabb ip, r4, r7, ip +#ifndef OPUS_ARM_FAST_ROUND + add r9, r9, #0x4000 +#endif +#ifndef OPUS_ARM_FAST_ROUND + add ip, ip, #0x4000 +#endif + mov r9, r9, asr #15 + mov ip, ip, asr #15 + strh ip, [r10, r2] + strh r9, [r10], #-2 + bne .Ler_bg +.Ler_done: + pop {r4-r10, pc} + .size exp_rotation1_armv5e, .-exp_rotation1_armv5e + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/arm/haar1_armv5e_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/haar1_armv5e_asm.S new file mode 100644 index 0000000000..68dc0248c0 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/haar1_armv5e_asm.S @@ -0,0 +1,89 @@ +/* ARMv5E haar1 for the CELT band recombination. + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2009 Xiph.Org Foundation + * Copyright (c) 2008-2009 Gregory Maxwell + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/bands.c are met. + * + * Why this exists + * --------------- + * The compiled pair loop hands each ldrh straight to smulbb, two two-cycle + * stalls per pair, and adds the rounding constant to both results. That + * stall accounts for all 15,216 of the interlock cycles the profile charges + * to haar1. Here both loads run ahead of their first use, and the rounding + * term joins the shared product once: + * + * (t1 + t2 + R) = (t1 + R) + t2, (t1 - t2 + R) = (t1 + R) - t2 + * + * Both loads stay. haar1 averages a stride of about 2, so the two elements + * are rarely adjacent and a packed word load would seldom apply. + * + * Bit-exact unless OPUS_ARM_FAST_ROUND is defined, which drops the rounding + * term and truncates instead. Measured against the rounded build, together + * with the same change in exp_rotation1: 742 of 15,360 decoded samples + * differ, at most 2 LSB, 83.1 dB. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "haar1_armv5e_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 + +/* ------------------------------------------------------------------------ + * void haar1_armv5e(celt_norm *X, int N0, int stride) + * + * For each i < stride and j < N0/2, with a = X[2j*stride+i] and + * b = X[(2j+1)*stride+i]: + * X[2j*stride+i] = (23170*a + 23170*b + 16384) >> 15 + * X[(2j+1)*stride+i] = (23170*a - 23170*b + 16384) >> 15 + * + * r0 X, r1 pairs, r2 stride, r3 stride in bytes, r4 pair step in bytes, + * r5 i, r6 walking pointer, r7 count, r8 and lr the two elements, r9 scratch, + * ip the constant. + * ------------------------------------------------------------------------ */ + .global haar1_armv5e + .type haar1_armv5e, %function +haar1_armv5e: + push {r4-r9, lr} + movs r1, r1, asr #1 @ N0 >>= 1 + ble .Lh1_done + cmp r2, #0 + ble .Lh1_done + mov r3, r2, lsl #1 + mov r4, r2, lsl #2 + mov ip, #0x5A00 + orr ip, ip, #0x82 @ QCONST16(.70710678f,15) = 23170 + mov r5, #0 +.Lh1_outer: + add r6, r0, r5, lsl #1 @ &X[i] + mov r7, r1 +.Lh1_pair: + ldrsh r8, [r6] @ a + ldrsh lr, [r6, r3] @ b + subs r7, r7, #1 @ loop test, and the gap before use + smulbb r8, ip, r8 @ t1 = 23170*a + smulbb lr, ip, lr @ t2 = 23170*b +#ifndef OPUS_ARM_FAST_ROUND + add r8, r8, #0x4000 @ t1 + R, shared by both results +#endif + add r9, r8, lr + sub r8, r8, lr + mov r9, r9, asr #15 + mov r8, r8, asr #15 + strh r8, [r6, r3] @ X[(2j+1)*stride+i] + strh r9, [r6], r4 @ X[2j*stride+i], then step a pair + bne .Lh1_pair + add r5, r5, #1 + cmp r5, r2 + blt .Lh1_outer +.Lh1_done: + pop {r4-r9, pc} + .size haar1_armv5e, .-haar1_armv5e + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/arm/normres_armv5e_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/normres_armv5e_asm.S new file mode 100644 index 0000000000..b1884eac6a --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/normres_armv5e_asm.S @@ -0,0 +1,81 @@ +/* ARMv5E inner loop for normalise_residual. + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2009 Xiph.Org Foundation + * Copyright (c) 2007-2009 Timothy B. Terriberry + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/vq.c are met. + * + * Why this exists + * --------------- + * The compiled loop is eight instructions per element: an ldr handed + * straight to smulbb, which stalls a cycle, the rounding add, a + * register-specified shift at two cycles, a separate counter increment and + * compare, the strh, and a taken branch. + * + * Here the counter is the loop test, each load runs ahead of its use, and + * the loop unrolls by two so the taken branch happens once per pair. The + * register shift stays: the shift is set per call from the energy, so there + * is no fixed form to specialise to. + * + * Bit-exact. iy is int, and smulbb reads exactly the low halfword that + * MULT16_16 casts it to. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "normres_armv5e_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 + +/* ------------------------------------------------------------------------ + * void normres_scale_armv5e(celt_norm *X, const int *iy, int N, int g, + * int shift) + * + * X[i] = (g*iy[i] + (1<>1)) >> shift for i in [0,N), 1 <= shift <= 31. + * N is at least 1, as in the do-while it replaces; a non-positive N writes + * nothing. + * + * r0 X, r1 iy, r2 count, r3 g, r4 rounding term, ip shift, r5 and lr the + * two elements of a pair. + * ------------------------------------------------------------------------ */ + .global normres_scale_armv5e + .type normres_scale_armv5e, %function +normres_scale_armv5e: + push {r4, r5, lr} + ldr ip, [sp, #12] @ shift + mov r4, #1 + mov r4, r4, lsl ip + mov r4, r4, lsr #1 @ 1<>1 + subs r2, r2, #2 @ r2 = elements left after the next pair + blt .Lnr_tail +.Lnr_pair: + ldr r5, [r1], #4 + ldr lr, [r1], #4 + subs r2, r2, #2 @ loop test, and the gap before use + smulbb r5, r3, r5 + smulbb lr, r3, lr + add r5, r5, r4 + add lr, lr, r4 + mov r5, r5, asr ip + mov lr, lr, asr ip + strh r5, [r0], #2 + strh lr, [r0], #2 + bge .Lnr_pair +.Lnr_tail: + adds r2, r2, #2 @ 0 or 1 left, or negative for N <= 0 + ble .Lnr_done + ldr r5, [r1], #4 + smulbb r5, r3, r5 + add r5, r5, r4 + mov r5, r5, asr ip + strh r5, [r0], #2 +.Lnr_done: + pop {r4, r5, pc} + .size normres_scale_armv5e, .-normres_scale_armv5e + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h new file mode 100644 index 0000000000..d45454fc61 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h @@ -0,0 +1,56 @@ +/* Copyright (c) 2025 Xiph.Org Foundation and contributors + Copyright (c) 2026 Michael Giacomelli + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef VQ_ARM_H +#define VQ_ARM_H + +/* Hand-written loops for celt/vq.c, through the OVERRIDE_vq_exp_rotation1 + hook vq.c already provides. Both architectures have one: the rotation + chain writes X[i+stride] and reads it straight back as the next x1, so at + stride 1 one of the two loads is redundant and one of the two stores is + dead, and both kernels carry that value in a register instead. Bit-exact; + see celt/arm/exp_rotation1_armv5e_asm.S. + + Shares OPUS_ARM_NO_BANDS_ASM with arm/bands_arm.h as the escape hatch. */ + +#if defined(FIXED_POINT) && !defined(OPUS_ARM_NO_BANDS_ASM) \ + && defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH == 5) + +#define OVERRIDE_vq_exp_rotation1 +void exp_rotation1_armv5e(celt_norm *X, int len, int stride, opus_val16 c, + opus_val16 s); +#define exp_rotation1(X, len, stride, c, s) \ + exp_rotation1_armv5e((X), (len), (stride), (c), (s)) + +/* The scaling loop at the end of normalise_residual. ARMv5E only: it wants + smulwb. See celt/arm/normres_armv5e_asm.S. */ +#define OVERRIDE_NORMRES_SCALE +void normres_scale_armv5e(celt_norm *X, const int *iy, int N, int g, + int shift); +#define NORMRES_SCALE normres_scale_armv5e + +#endif + +#endif /* VQ_ARM_H */ diff --git a/lib/rbcodec/codecs/libopus/celt/bands.c b/lib/rbcodec/codecs/libopus/celt/bands.c index 186468568c..eec3dd0f19 100644 --- a/lib/rbcodec/codecs/libopus/celt/bands.c +++ b/lib/rbcodec/codecs/libopus/celt/bands.c @@ -42,6 +42,9 @@ #include "rate.h" #include "quant_bands.h" #include "pitch.h" +#if defined(OPUS_ARM_ASM) +#include "arm/bands_arm.h" +#endif int hysteresis_decision(opus_val16 val, const opus_val16 *thresholds, const opus_val16 *hysteresis, int N, int prev) { @@ -256,9 +259,17 @@ void denormalise_bands(const CELTMode *m, const celt_norm * OPUS_RESTRICT X, } else #endif /* Be careful of the fixed-point "else" just above when changing this code */ +#if defined(FIXED_POINT) && defined(OVERRIDE_DENORM_BAND) + { + DENORM_BAND(f, x, band_end-j, g, shift); + f += band_end-j; + x += band_end-j; + } +#else do { *f++ = SHR32(MULT16_16(*x++, g), shift); } while (++j Date: Fri, 18 Sep 2026 11:59:23 -0400 Subject: [PATCH 207/408] opus: ARM comb filter and saturation kernels comb_filter_const on both cores, and celt_synthesis's SIG_SAT clamp four samples at a time through one ldm and one stm. Modelled for the saturation loop: -1.05% ARMv4, -1.42% ARMv5E. Co-Authored-By: Claude Opus 5 Change-Id: I381afab451eb29a91513ce6ab29c1b2d565b7ee9 --- lib/rbcodec/codecs/libopus/SOURCES | 2 + .../codecs/libopus/celt/arm/comb_filter_arm.h | 74 +++++++ .../libopus/celt/arm/comb_filter_armv4_asm.S | 208 ++++++++++++++++++ .../libopus/celt/arm/comb_filter_armv5e_asm.S | 178 +++++++++++++++ lib/rbcodec/codecs/libopus/celt/celt.h | 4 + .../codecs/libopus/celt/celt_decoder.c | 13 +- 6 files changed, 477 insertions(+), 2 deletions(-) create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/comb_filter_armv4_asm.S create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/comb_filter_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index fef8a216f1..aa726b4c22 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -11,8 +11,10 @@ celt/kiss_fft.c #if defined(CPU_ARM) && (ARM_ARCH == 4) celt/arm/kiss_fft_armv4_asm.S celt/arm/mdct_armv4_asm.S +celt/arm/comb_filter_armv4_asm.S celt/arm/denorm_armv4_asm.S #elif defined(CPU_ARM) && (ARM_ARCH == 5) +celt/arm/comb_filter_armv5e_asm.S celt/arm/denorm_armv5e_asm.S celt/arm/exp_rotation1_armv5e_asm.S celt/arm/haar1_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h new file mode 100644 index 0000000000..7d24a1d204 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h @@ -0,0 +1,74 @@ +/* Copyright (c) 2025 Xiph.Org Foundation and contributors + Copyright (c) 2026 Michael Giacomelli + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef COMB_FILTER_ARM_H +#define COMB_FILTER_ARM_H + +/* Hand-written comb_filter_const for both ARM generations, reached through + the OVERRIDE_COMB_FILTER_CONST hook celt.h already provides. Both are + bit-exact with the C they replace; see the assembly for why the reordered + accumulation is exact rather than merely close. + + Neither implements the CUSTOM_MODES scalar tail, so the override stands + aside when that is defined. Build with OPUS_ARM_NO_COMB_ASM to select the + C version instead, which is how the two are compared. */ + +#if defined(FIXED_POINT) && !defined(CUSTOM_MODES) \ + && !defined(OPUS_ARM_NO_COMB_ASM) + +# if defined(OPUS_ARM_INLINE_ASM) && (ARM_ARCH == 4) + +# define OVERRIDE_COMB_FILTER_CONST +void comb_filter_const_armv4(opus_val32 *y, opus_val32 *x, int T, int N, + opus_val16 g10, opus_val16 g11, opus_val16 g12); +# define comb_filter_const(y, x, T, N, g10, g11, g12, arch) \ + ((void)(arch), comb_filter_const_armv4((y), (x), (T), (N), \ + (g10), (g11), (g12))) + +/* The SIG_SAT guard celt_synthesis applies to the IMDCT output before the + postfilter reads it; lives with the comb filter it protects. Replaces the + static inline celt_sat in celt_decoder.c. */ +# define OVERRIDE_CELT_SAT +void celt_sat_armv4(celt_sig *x, int n); +# define celt_sat(x, n) celt_sat_armv4((x), (n)) + +# elif defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH == 5) + +# define OVERRIDE_COMB_FILTER_CONST +void comb_filter_const_armv5e(opus_val32 *y, opus_val32 *x, int T, int N, + opus_val16 g10, opus_val16 g11, opus_val16 g12); +# define comb_filter_const(y, x, T, N, g10, g11, g12, arch) \ + ((void)(arch), comb_filter_const_armv5e((y), (x), (T), (N), \ + (g10), (g11), (g12))) + +# define OVERRIDE_CELT_SAT +void celt_sat_armv5e(celt_sig *x, int n); +# define celt_sat(x, n) celt_sat_armv5e((x), (n)) + +# endif + +#endif + +#endif /* COMB_FILTER_ARM_H */ diff --git a/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_armv4_asm.S new file mode 100644 index 0000000000..eb27bc32a4 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_armv4_asm.S @@ -0,0 +1,208 @@ +/* ARMv4 inner loop for the CELT comb filter (pitch postfilter). + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2008 Xiph.Org Foundation + * Copyright (c) 2008 Gregory Maxwell + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/celt.c are met. + * + * Why this exists + * --------------- + * comb_filter_const_c is a 5-tap symmetric FIR on a delayed copy of the + * signal, unrolled five ways so the delay window rotates through registers + * instead of shifting. That needs five window values, three gains, three + * pointers and a saturation bound live at once, which is twelve of the + * fourteen registers before there is anywhere to accumulate. gcc gives up + * and spills the gains, so the compiled loop reloads all three of them and + * the input pointer from the stack for every single output sample: + * + * ldr r1, [sp, #4] @ the x pointer + * ldr r8, [r1] + * ldr r10, [sp, #8] @ g10 + * smull r1, r9, r0, r10 + * ... + * ldr fp, [sp, #24] @ g11 + * ... + * ldr r4, [sp, #12] @ g12 + * + * Nineteen instructions per sample, four of them reloading invariants, and + * 59.5% of the function's executed memory traffic is stack rather than + * signal. Here everything stays resident and the only stack access is one + * reload of the loop limit per group of five. + * + * Two things make that fit. The multiply-accumulates are reordered to run + * outer pair, inner pair, centre tap; each product is truncated inside its + * own instruction exactly as the C does, and summing the same three + * truncated values in a different order gives the same 32-bit result, so + * this is bit-exact rather than merely close. And the window register that + * the next output is about to overwrite is dead as soon as the outer pair + * has been formed, so it serves as the scratch the multiplies need. + * + * Note the loop bound. Without CUSTOM_MODES the C loop is + * "for (i=0;i> 16) + * + * where the window values are already doubled. \new is the slot the fresh + * delayed sample lands in, \dying the slot the next sample will overwrite, + * which is also the second term of the outer pair and, once that pair has + * been consumed, the scratch register for the multiplies. + * + * MULT16_32_Q16_armv4 puts the 16-bit operand in the top half of Rs and + * keeps the high word of the product, so the gains are pre-shifted. Using + * the gain as Rm and the data as Rs lets Rs double as RdLo, which ARMv4 + * permits and which is what frees the third register. + */ + .macro CF_ONE new, dying, center, p1a, p1b + ldr lr, [r2], #4 @ the fresh delayed sample, undoubled + ldr ip, [r1], #4 @ t = x[i] + add \dying, \dying, lr, lsl #1 @ outer pair, into the dying slot + mov \new, lr, lsl #1 @ and the window slot keeps it doubled + smull \dying, lr, r11, \dying @ lr = (g12 * outer) >> 16 + add ip, ip, lr + add \dying, \p1a, \p1b @ inner pair + smull \dying, lr, r10, \dying @ lr = (g11 * inner) >> 16 + add ip, ip, lr + smull \dying, lr, r9, \center @ lr = (g10 * centre) >> 16 + add ip, ip, lr + cmp ip, r3 @ SATURATE(t, SIG_SAT), branchless and + movgt ip, r3 @ with only the positive bound resident + cmn ip, r3 + rsblt ip, r3, #0 + str ip, [r0], #4 + .endm + +/* ------------------------------------------------------------------------ + * void comb_filter_const_armv4(opus_val32 *y, opus_val32 *x, int T, int N, + * opus_val16 g10, opus_val16 g11, opus_val16 g12) + * + * r0 y, r1 x, r2 the delayed read pointer, r3 SIG_SAT, r4-r8 the window, + * r9-r11 the pre-shifted gains, ip the accumulator, lr scratch. The loop + * limit is the one thing that does not fit, and it is read once per group. + * ------------------------------------------------------------------------ */ + .global comb_filter_const_armv4 + .type comb_filter_const_armv4, %function +comb_filter_const_armv4: + push {r4-r11, lr} + sub sp, sp, #4 + ldrsh r9, [sp, #40] @ g10 + ldrsh r10, [sp, #44] @ g11 + ldrsh r11, [sp, #48] @ g12 + mov r9, r9, lsl #16 + mov r10, r10, lsl #16 + mov r11, r11, lsl #16 + cmp r3, #5 @ a short or empty span does nothing, + blt .Lcf_done @ and keeps N out of the unsigned divide + /* groups = N/5, by the usual reciprocal, so the loop needs no counter */ + ldr lr, .Lcf_recip + umull ip, r4, r3, lr @ RdHi, RdLo and Rm must all differ + movs r4, r4, lsr #2 + beq .Lcf_done + add r4, r4, r4, lsl #2 @ 5 * groups + add r4, r1, r4, lsl #2 @ x once the last group has been read + str r4, [sp] + /* Prime the window from x[-T-2 .. -T+1], doubled, and leave the read + pointer at x[-T+2], which is the first fresh sample. */ + sub r2, r1, r2, lsl #2 @ &x[-T] + sub r2, r2, #8 @ &x[-T-2] + ldm r2!, {r5, r6, r7, r8} @ X4, X3, X2, X1 in address order + mov r5, r5, lsl #1 + mov r6, r6, lsl #1 + mov r7, r7, lsl #1 + mov r8, r8, lsl #1 + ldr r3, .Lcf_sat +.Lcf_loop: + CF_ONE r4, r5, r7, r8, r6 @ X0 = new, X4 dies, centre X2 + CF_ONE r5, r6, r8, r4, r7 @ X4 = new, X3 dies, centre X1 + CF_ONE r6, r7, r4, r5, r8 @ X3 = new, X2 dies, centre X0 + CF_ONE r7, r8, r5, r6, r4 @ X2 = new, X1 dies, centre X4 + CF_ONE r8, r4, r6, r7, r5 @ X1 = new, X0 dies, centre X3 + ldr lr, [sp] + cmp r1, lr + bne .Lcf_loop +.Lcf_done: + add sp, sp, #4 + pop {r4-r11, pc} + .size comb_filter_const_armv4, .-comb_filter_const_armv4 + + .align 2 +.Lcf_sat: + .word 300000000 @ SIG_SAT +.Lcf_recip: + .word 0xCCCCCCCD @ N/5 = (N * this) >> 34 + +/* ------------------------------------------------------------------------ + * void celt_sat_armv4(celt_sig *x, int n) + * + * x[i] = SATURATE(x[i], SIG_SAT) in place, the guard celt_synthesis puts on + * the IMDCT output before the postfilter reads it. gcc compiles it one + * sample at a time: ldr, two compare-and-move clamps against register-held + * bounds, a compare, str and a taken branch, about 13 cycles a sample. Four + * at a time, one ldm and one stm replace four ldr and four str, and the + * clamp needs only the positive bound. About 8 cycles a sample. + * ------------------------------------------------------------------------ */ + .macro SAT_ONE reg + cmp \reg, r3 + movgt \reg, r3 + cmn \reg, r3 + rsblt \reg, r3, #0 + .endm + + .align 2 + .global celt_sat_armv4 + .type celt_sat_armv4, %function +celt_sat_armv4: + push {r4-r7, lr} + ldr r3, .Lcf_sat + cmp r1, #4 + blt .Lsat4_tail +.Lsat4_four: + ldmia r0, {r4, r5, r6, r7} + sub r1, r1, #4 @ no flags: the clamps below set them + SAT_ONE r4 + SAT_ONE r5 + SAT_ONE r6 + SAT_ONE r7 + stmia r0!, {r4, r5, r6, r7} + cmp r1, #4 + bge .Lsat4_four +.Lsat4_tail: + cmp r1, #0 + ble .Lsat4_done +.Lsat4_one: + ldr r4, [r0] + sub r1, r1, #1 + SAT_ONE r4 + str r4, [r0], #4 + cmp r1, #0 + bgt .Lsat4_one +.Lsat4_done: + pop {r4-r7, pc} + .size celt_sat_armv4, .-celt_sat_armv4 + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_armv5e_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_armv5e_asm.S new file mode 100644 index 0000000000..89b9057af0 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_armv5e_asm.S @@ -0,0 +1,178 @@ +/* ARMv5E inner loop for the CELT comb filter (pitch postfilter). + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2008 Xiph.Org Foundation + * Copyright (c) 2008 Gregory Maxwell + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/celt.c are met. + * + * Why this exists + * --------------- + * The same register shortage as the ARMv4 kernel, described in + * comb_filter_armv4_asm.S, and the same fix. On this architecture 47.7% of + * the function's executed memory traffic is stack rather than signal. + * + * Two things are cheaper here. MAC16_32_Q16 is a single smlawb, so the + * multiplies need no scratch pair and no pre-shifted gain, and two of the + * three gains ride in one register as a halfword pair that smlawb and + * smlawt select between. That buys back enough room to keep the loop limit + * resident too, so this loop touches the stack not at all. + * + * What does not help here is bursting. The loop reads five consecutive + * words at each of two places and writes five at a third, which looks like + * three ideal multi-register transfers, but holding five results at once + * needs five registers that the window and the gains have already spent, + * and on this core an ldm of n costs the same n cycles as n separate loads + * anyway. So the memory traffic stays one word at a time on purpose. + * + * The multiply-accumulates run outer pair, inner pair, centre tap rather + * than the C's order. Each product is truncated inside its own + * instruction, exactly as in the C, and summing the same three truncated + * values in a different order gives the same 32-bit result, so this is + * bit-exact. + * + * Note the loop bound. Without CUSTOM_MODES the C loop is + * "for (i=0;i> 16) + * + * where the window values are already doubled. \new is the slot the fresh + * delayed sample lands in and \dying the slot the next sample overwrites, + * which is also the second term of the outer pair and, once that pair is + * consumed, the scratch the inner pair is built in. + */ + .macro CF_ONE new, dying, center, p1a, p1b + ldr lr, [r2], #4 @ the fresh delayed sample, undoubled + ldr ip, [r1], #4 @ t = x[i] + add \dying, \dying, lr, lsl #1 @ outer pair, into the dying slot + mov \new, lr, lsl #1 @ and the window slot keeps it doubled + smlawb ip, \dying, r10, ip @ t += (outer * g12) >> 16 + add \dying, \p1a, \p1b @ inner pair + smlawt ip, \dying, r9, ip @ t += (inner * g11) >> 16 + smlawb ip, \center, r9, ip @ t += (centre * g10) >> 16 + cmp ip, r3 @ SATURATE(t, SIG_SAT), branchless and + movgt ip, r3 @ with only the positive bound resident + cmn ip, r3 + rsblt ip, r3, #0 + str ip, [r0], #4 + .endm + +/* ------------------------------------------------------------------------ + * void comb_filter_const_armv5e(opus_val32 *y, opus_val32 *x, int T, int N, + * opus_val16 g10, opus_val16 g11, + * opus_val16 g12) + * + * r0 y, r1 x, r2 the delayed read pointer, r3 SIG_SAT, r4-r8 the window, + * r9 g10 packed under g11, r10 g12, r11 the loop limit, ip the accumulator, + * lr scratch. Fourteen registers, nothing spilled. + * ------------------------------------------------------------------------ */ + .global comb_filter_const_armv5e + .type comb_filter_const_armv5e, %function +comb_filter_const_armv5e: + push {r4-r11, lr} + ldrh r9, [sp, #36] @ g10, zero-extended: smlawb reads 15:0 + ldrsh lr, [sp, #40] @ g11 + ldrsh r10, [sp, #44] @ g12 + orr r9, r9, lr, lsl #16 @ g10 low, g11 high + cmp r3, #5 @ a short or empty span does nothing, + blt .Lcf5_done @ and keeps N out of the unsigned divide + /* groups = N/5, by the usual reciprocal, so the loop needs no counter */ + ldr lr, .Lcf5_recip + umull ip, r11, r3, lr @ RdHi, RdLo and Rm must all differ + movs r11, r11, lsr #2 + beq .Lcf5_done + add r11, r11, r11, lsl #2 @ 5 * groups + add r11, r1, r11, lsl #2 @ x once the last group has been read + /* Prime the window from x[-T-2 .. -T+1], doubled, and leave the read + pointer at x[-T+2], which is the first fresh sample. */ + sub r2, r1, r2, lsl #2 @ &x[-T] + sub r2, r2, #8 @ &x[-T-2] + ldm r2!, {r5, r6, r7, r8} @ X4, X3, X2, X1 in address order + mov r5, r5, lsl #1 + mov r6, r6, lsl #1 + mov r7, r7, lsl #1 + mov r8, r8, lsl #1 + ldr r3, .Lcf5_sat +.Lcf5_loop: + CF_ONE r4, r5, r7, r8, r6 @ X0 = new, X4 dies, centre X2 + CF_ONE r5, r6, r8, r4, r7 @ X4 = new, X3 dies, centre X1 + CF_ONE r6, r7, r4, r5, r8 @ X3 = new, X2 dies, centre X0 + CF_ONE r7, r8, r5, r6, r4 @ X2 = new, X1 dies, centre X4 + CF_ONE r8, r4, r6, r7, r5 @ X1 = new, X0 dies, centre X3 + cmp r1, r11 + bne .Lcf5_loop +.Lcf5_done: + pop {r4-r11, pc} + .size comb_filter_const_armv5e, .-comb_filter_const_armv5e + + .align 2 +.Lcf5_sat: + .word 300000000 @ SIG_SAT +.Lcf5_recip: + .word 0xCCCCCCCD @ N/5 = (N * this) >> 34 + +/* ------------------------------------------------------------------------ + * void celt_sat_armv5e(celt_sig *x, int n) + * + * x[i] = SATURATE(x[i], SIG_SAT) in place, the guard celt_synthesis puts on + * the IMDCT output before the postfilter reads it. gcc compiles it one + * sample at a time, and the ldr feeds the first compare directly, which + * stalls. Four at a time here: an ldm, then a sub that sets no flags and so + * keeps the loaded values clear of their first use, four single-bound + * clamps and an stm. About 7.25 cycles a sample against 11. + * ------------------------------------------------------------------------ */ + .macro SAT_ONE reg + cmp \reg, r3 + movgt \reg, r3 + cmn \reg, r3 + rsblt \reg, r3, #0 + .endm + + .align 2 + .global celt_sat_armv5e + .type celt_sat_armv5e, %function +celt_sat_armv5e: + push {r4-r7, lr} + ldr r3, .Lcf5_sat + cmp r1, #4 + blt .Lsat5_tail +.Lsat5_four: + ldmia r0, {r4, r5, r6, r7} + sub r1, r1, #4 @ the gap, and no flags to disturb + SAT_ONE r4 + SAT_ONE r5 + SAT_ONE r6 + SAT_ONE r7 + stmia r0!, {r4, r5, r6, r7} + cmp r1, #4 + bge .Lsat5_four +.Lsat5_tail: + cmp r1, #0 + ble .Lsat5_done +.Lsat5_one: + ldr r4, [r0] + sub r1, r1, #1 + SAT_ONE r4 + str r4, [r0], #4 + cmp r1, #0 + bgt .Lsat5_one +.Lsat5_done: + pop {r4-r7, pc} + .size celt_sat_armv5e, .-celt_sat_armv5e + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/celt.h b/lib/rbcodec/codecs/libopus/celt/celt.h index 24b6b2b520..90a464e39b 100644 --- a/lib/rbcodec/codecs/libopus/celt/celt.h +++ b/lib/rbcodec/codecs/libopus/celt/celt.h @@ -230,6 +230,10 @@ void comb_filter_const_c(opus_val32 *y, opus_val32 *x, int T, int N, opus_val16 g10, opus_val16 g11, opus_val16 g12); #endif +#if defined(OPUS_ARM_ASM) +#include "arm/comb_filter_arm.h" +#endif + #ifndef OVERRIDE_COMB_FILTER_CONST # define comb_filter_const(y, x, T, N, g10, g11, g12, arch) \ ((void)(arch),comb_filter_const_c(y, x, T, N, g10, g11, g12)) diff --git a/lib/rbcodec/codecs/libopus/celt/celt_decoder.c b/lib/rbcodec/codecs/libopus/celt/celt_decoder.c index e6efce9358..6aef870c07 100644 --- a/lib/rbcodec/codecs/libopus/celt/celt_decoder.c +++ b/lib/rbcodec/codecs/libopus/celt/celt_decoder.c @@ -357,6 +357,16 @@ void deemphasis(celt_sig *in[], opus_val16 *pcm, int N, int C, int downsample, c RESTORE_STACK; } +#ifndef OVERRIDE_CELT_SAT +/* Clamp n samples to +/-SIG_SAT in place. */ +static OPUS_INLINE void celt_sat(celt_sig *x, int n) +{ + int i; + for (i=0;i Date: Tue, 22 Sep 2026 17:59:47 +0300 Subject: [PATCH 208/408] rbutilqt/themeeditor: Fix description of KEYCHAIN_PROFILE Change-Id: I993da51351c01b35fe49cfca3750f2757c5c21ce --- utils/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 12647bbfba..6563445511 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -143,7 +143,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") # Submitting the .dmg file to Apple's service to get a # notarization ticket for distributing outside the App store set(KEYCHAIN_PROFILE "" CACHE STRING - "Code signing identity to use for macOS signing") + "Apple Keychain profile name to use for notarization") if(DEFINED ENV{KEYCHAIN_PROFILE}) set(KEYCHAIN_PROFILE "$ENV{KEYCHAIN_PROFILE}") From 63978def70e5d0e478c73dd61ef723de7f988dcf Mon Sep 17 00:00:00 2001 From: Paul Sauro Date: Sun, 20 Sep 2026 17:21:42 +0200 Subject: [PATCH 209/408] usb arc: discard stale transfers and audio work on bus reset If RESET and IOC/SOF are reported together, the old interrupt order can refill audio or report completions from descriptors invalidated by the reset. Handle RESET first and return without dispatching those stale events. Disable SOF refill and stop the batch ring when resetting the controller. The register-stub regression exercises simultaneous RESET, IOC and SOF and checks that no old completion or refill is dispatched. Change-Id: Iac98df59aea30dfee155302f7ad31c6ba9902975 --- firmware/target/arm/usb-drv-arc.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/firmware/target/arm/usb-drv-arc.c b/firmware/target/arm/usb-drv-arc.c index dd46d67dd0..c3bc9e093b 100644 --- a/firmware/target/arm/usb-drv-arc.c +++ b/firmware/target/arm/usb-drv-arc.c @@ -553,6 +553,15 @@ void usb_drv_int(void) unsigned int usbintr = REG_USBINTR; /* Only watch enabled ints */ unsigned int status = REG_USBSTS & usbintr; + /* Reset invalidates all old completions, including the audio ring. + * Do not dispatch a simultaneous SOF or IOC using stale descriptors. */ + if (status & USBSTS_RESET) { + REG_USBSTS = status; + bus_reset(); + usb_core_bus_reset(); + return; + } + #if 0 if (status & USBSTS_INT) logf("int: usb ioc"); if (status & USBSTS_ERR) logf("int: usb err"); @@ -579,13 +588,6 @@ void usb_drv_int(void) logf("usb error int"); } - /* reset interrupt */ - if (status & USBSTS_RESET) { - REG_USBSTS = USBSTS_RESET; - bus_reset(); - usb_core_bus_reset(); /* tell mom */ - } - /* port change */ if (status & USBSTS_PORT_CHANGE) { REG_USBSTS = USBSTS_PORT_CHANGE; @@ -1155,6 +1157,9 @@ static void bus_reset(void) int i; logf("usb bus_reset"); + REG_USBINTR &= ~USBINTR_SOF_EN; + batch_stopped = true; + REG_DEVICEADDR = 0; pending_device_address = -1; REG_ENDPTSETUPSTAT = REG_ENDPTSETUPSTAT; From b1385d831e85cf9681156cc05b972c400cb219a2 Mon Sep 17 00:00:00 2001 From: Paul Sauro Date: Sun, 20 Sep 2026 17:21:42 +0200 Subject: [PATCH 210/408] usb arc: reset endpoint data toggles when clearing halt CLEAR_FEATURE(ENDPOINT_HALT) must reset the selected non-control endpoint data toggle as well as removing STALL. Set the corresponding ARC toggle-reset bit when clearing the halt, including when the endpoint is already unstalled. EP0 keeps its SETUP-controlled toggle handling. The register-stub test checks IN/OUT independently and verifies that EP0 does not receive a non-control toggle reset. Change-Id: I18bce488ec250336512bfda4db6ff369d21d9180 --- firmware/target/arm/usb-drv-arc.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/firmware/target/arm/usb-drv-arc.c b/firmware/target/arm/usb-drv-arc.c index c3bc9e093b..20bd895f65 100644 --- a/firmware/target/arm/usb-drv-arc.c +++ b/firmware/target/arm/usb-drv-arc.c @@ -621,7 +621,10 @@ void usb_drv_stall(int endpoint, bool stall, bool in) REG_ENDPTCTRL(ep_num) |= EPCTRL_TX_EP_STALL; } else { - REG_ENDPTCTRL(ep_num) &= ~EPCTRL_TX_EP_STALL; + /* CLEAR_FEATURE(ENDPOINT_HALT) also resets the data toggle. + * EP0 owns its toggle through SETUP, not these register bits. */ + REG_ENDPTCTRL(ep_num) = (REG_ENDPTCTRL(ep_num) & ~EPCTRL_TX_EP_STALL) | + (ep_num ? EPCTRL_TX_DATA_TOGGLE_RST : 0); } } else { @@ -629,7 +632,8 @@ void usb_drv_stall(int endpoint, bool stall, bool in) REG_ENDPTCTRL(ep_num) |= EPCTRL_RX_EP_STALL; } else { - REG_ENDPTCTRL(ep_num) &= ~EPCTRL_RX_EP_STALL; + REG_ENDPTCTRL(ep_num) = (REG_ENDPTCTRL(ep_num) & ~EPCTRL_RX_EP_STALL) | + (ep_num ? EPCTRL_RX_DATA_TOGGLE_RST : 0); } } } From 29eef25ac7e782db6ddd0f8daa7e72418eb7c488 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Wed, 23 Sep 2026 11:38:14 +0100 Subject: [PATCH 211/408] hw_h264: use standard rockbox copyright headers Some files were not using the standard header with the Rockbox logo. Add this and move any technical notes into a separate comment. Change-Id: Idaac932cd56154c7b785ba0e6c0231a21878f786 --- apps/plugins/h264_player/video_audio.c | 28 +++++++++++++++++------ apps/plugins/h264_player/video_audio.h | 22 ++++++++++++++---- apps/plugins/h264_player/video_pcm.c | 28 +++++++++++++++++------ apps/plugins/h264_player/video_pcm.h | 21 +++++++++++++---- apps/plugins/h264_player/video_playback.c | 28 ++++++++++++++++++----- apps/plugins/h264_player/video_playback.h | 15 +++++++++++- firmware/export/hw_h264.h | 12 ++++++++-- 7 files changed, 123 insertions(+), 31 deletions(-) diff --git a/apps/plugins/h264_player/video_audio.c b/apps/plugins/h264_player/video_audio.c index 9bd684907b..53c43264a4 100644 --- a/apps/plugins/h264_player/video_audio.c +++ b/apps/plugins/h264_player/video_audio.c @@ -1,11 +1,10 @@ /*************************************************************************** - * AAC audio service for the native S5L8702 H.264 player. - * - * The decoder itself remains Rockbox's normal dynamically-loaded aac.codec. - * That is important on iPod 6G: FAAD's hot code and working state are an IRAM - * overlay and cannot be linked into the permanent core without overflowing - * IRAM. This file borrows the existing codec thread, supplies file-buffer - * callbacks for the MP4, and sends decoded PCM to video_pcm. + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ * * Copyright (C) 2025-2026 David Cormier * @@ -13,7 +12,22 @@ * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * ****************************************************************************/ + +/* + * AAC audio service for the native S5L8702 H.264 player. + * + * The decoder itself remains Rockbox's normal dynamically-loaded aac.codec. + * That is important on iPod 6G: FAAD's hot code and working state are an IRAM + * overlay and cannot be linked into the permanent core without overflowing + * IRAM. This file borrows the existing codec thread, supplies file-buffer + * callbacks for the MP4, and sends decoded PCM to video_pcm. + */ + #include "plugin.h" #ifdef HAVE_HW_H264 diff --git a/apps/plugins/h264_player/video_audio.h b/apps/plugins/h264_player/video_audio.h index 69c229804a..9af846abb5 100644 --- a/apps/plugins/h264_player/video_audio.h +++ b/apps/plugins/h264_player/video_audio.h @@ -1,8 +1,10 @@ /*************************************************************************** - * H.264 player audio decode service. - * - * Decodes AAC audio via Rockbox's aac.codec and feeds a PCM ring buffer. - * Uses the audio clock from video_pcm for A/V synchronization. + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ * * Copyright (C) 2025-2026 David Cormier * @@ -10,10 +12,22 @@ * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * ****************************************************************************/ #ifndef VIDEO_AUDIO_H #define VIDEO_AUDIO_H +/* + * H.264 player audio decode service. + * + * Decodes AAC audio via Rockbox's aac.codec and feeds a PCM ring buffer. + * Uses the audio clock from video_pcm for A/V synchronization. + * + */ + #include #include #include diff --git a/apps/plugins/h264_player/video_pcm.c b/apps/plugins/h264_player/video_pcm.c index 72874bc9c4..fd46692bb2 100644 --- a/apps/plugins/h264_player/video_pcm.c +++ b/apps/plugins/h264_player/video_pcm.c @@ -1,11 +1,10 @@ /*************************************************************************** - * PCM ring buffer for video player audio output. - * - * Simplified version of mpegplayer's pcm_output.c. - * Single-producer (audio decode thread), single-consumer (DMA ISR). - * Audio-master clock via monotonic sample counter. - * - * get_more() runs in DMA ISR context: no yield, sleep, mutex, or file I/O. + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ * * Copyright (C) 2025-2026 David Cormier * @@ -13,7 +12,22 @@ * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * ****************************************************************************/ + +/* + * PCM ring buffer for video player audio output. + * + * Simplified version of mpegplayer's pcm_output.c. + * Single-producer (audio decode thread), single-consumer (DMA ISR). + * Audio-master clock via monotonic sample counter. + * + * get_more() runs in DMA ISR context: no yield, sleep, mutex, or file I/O. + */ + #include "plugin.h" #ifdef HAVE_HW_H264 diff --git a/apps/plugins/h264_player/video_pcm.h b/apps/plugins/h264_player/video_pcm.h index 964a913597..671341ae34 100644 --- a/apps/plugins/h264_player/video_pcm.h +++ b/apps/plugins/h264_player/video_pcm.h @@ -1,8 +1,10 @@ /*************************************************************************** - * PCM ring buffer for H.264 player audio output. - * - * Simplified version of mpegplayer's pcm_output.c. - * Audio-master clock: DMA callback advances clock_samples monotonically. + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ * * Copyright (C) 2025-2026 David Cormier * @@ -10,10 +12,21 @@ * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * ****************************************************************************/ #ifndef VIDEO_PCM_H #define VIDEO_PCM_H +/* + * PCM ring buffer for H.264 player audio output. + * + * Simplified version of mpegplayer's pcm_output.c. + * Audio-master clock: DMA callback advances clock_samples monotonically. + */ + #include #include #include diff --git a/apps/plugins/h264_player/video_playback.c b/apps/plugins/h264_player/video_playback.c index b366079da7..f7f7ff73e4 100644 --- a/apps/plugins/h264_player/video_playback.c +++ b/apps/plugins/h264_player/video_playback.c @@ -1,4 +1,24 @@ /*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* * S5L8702 hardware H.264 player. * * This is deliberately a narrow production contract: non-fragmented MP4/M4V, @@ -7,12 +27,8 @@ * <= 640x480, <= 30 fps, with optional AAC-LC audio. * * Copyright (C) 2025-2026 David Cormier - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - ****************************************************************************/ + */ + #include "plugin.h" #ifdef HAVE_HW_H264 diff --git a/apps/plugins/h264_player/video_playback.h b/apps/plugins/h264_player/video_playback.h index 2433c44396..351e0cad4c 100644 --- a/apps/plugins/h264_player/video_playback.h +++ b/apps/plugins/h264_player/video_playback.h @@ -1,5 +1,10 @@ /*************************************************************************** - * Hardware H.264 player implementation. + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ * * Copyright (C) 2025-2026 David Cormier * @@ -7,10 +12,18 @@ * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * ****************************************************************************/ #ifndef VIDEO_PLAYBACK_H #define VIDEO_PLAYBACK_H +/* + * Hardware H.264 player implementation. + */ + #include /* Play a validated H.264 Baseline/AAC-LC MP4 from caller-owned memory. diff --git a/firmware/export/hw_h264.h b/firmware/export/hw_h264.h index d7a403284f..affb132978 100644 --- a/firmware/export/hw_h264.h +++ b/firmware/export/hw_h264.h @@ -1,5 +1,10 @@ /*************************************************************************** - * Hardware H.264 decoder interface exposed to viewer plugins. + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ * * Copyright (C) 2025-2026 David Cormier * @@ -7,8 +12,11 @@ * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * ****************************************************************************/ - #ifndef HW_H264_H #define HW_H264_H From d27af08ff6b4fc6e0d651bcc7927d1f3b98f6a5b Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Wed, 23 Sep 2026 11:46:43 +0100 Subject: [PATCH 212/408] hw_h264: remove redundant ifdef guards Change-Id: I5fa1abc1226ec5a35888713da83467a71a3c4caf --- apps/plugin.c | 3 --- apps/plugin.h | 2 -- apps/plugins/h264_player/video_audio.c | 5 ----- apps/plugins/h264_player/video_pcm.c | 5 ----- apps/plugins/h264_player/video_playback.c | 6 ------ firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c | 6 ------ 6 files changed, 27 deletions(-) diff --git a/apps/plugin.c b/apps/plugin.c index 83574d5ed0..ec42d25a61 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -49,9 +49,6 @@ #include "language.h" #include "statusbar-skinned.h" #include "panic.h" -#ifdef HAVE_HW_H264 -#include "hw_h264.h" -#endif #if CONFIG_CHARGING #include "power.h" diff --git a/apps/plugin.h b/apps/plugin.h index e98c4b78a3..ab60404a3b 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -83,9 +83,7 @@ int plugin_open(const char *plugin, const char *parameter); #include "metadata.h" #include "sound.h" #include "audio.h" -#ifdef HAVE_HW_H264 #include "hw_h264.h" -#endif #include "voice_thread.h" #include "root_menu.h" #include "talk.h" diff --git a/apps/plugins/h264_player/video_audio.c b/apps/plugins/h264_player/video_audio.c index 53c43264a4..dd6a786086 100644 --- a/apps/plugins/h264_player/video_audio.c +++ b/apps/plugins/h264_player/video_audio.c @@ -29,9 +29,6 @@ */ #include "plugin.h" - -#ifdef HAVE_HW_H264 - #include #include "codec_thread.h" @@ -573,5 +570,3 @@ bool video_audio_failed(void) { return audio_has_error; } - -#endif /* HAVE_HW_H264 */ diff --git a/apps/plugins/h264_player/video_pcm.c b/apps/plugins/h264_player/video_pcm.c index fd46692bb2..82305f2e53 100644 --- a/apps/plugins/h264_player/video_pcm.c +++ b/apps/plugins/h264_player/video_pcm.c @@ -29,9 +29,6 @@ */ #include "plugin.h" - -#ifdef HAVE_HW_H264 - #include "system.h" #include "kernel.h" #include "pcm.h" @@ -260,5 +257,3 @@ uint32_t video_pcm_buffered_samples(void) { return pcm_used(); } - -#endif /* HAVE_HW_H264 */ diff --git a/apps/plugins/h264_player/video_playback.c b/apps/plugins/h264_player/video_playback.c index f7f7ff73e4..b73cb7694f 100644 --- a/apps/plugins/h264_player/video_playback.c +++ b/apps/plugins/h264_player/video_playback.c @@ -30,9 +30,6 @@ */ #include "plugin.h" - -#ifdef HAVE_HW_H264 - #include "backlight.h" #include "button.h" #include "crc32.h" @@ -54,7 +51,6 @@ #include "video_audio.h" #include "video_pcm.h" #include "video_playback.h" -#include "hw_h264.h" #include #include @@ -1144,5 +1140,3 @@ cleanup: rb->cpu_boost(false); return result; } - -#endif /* HAVE_HW_H264 */ diff --git a/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c b/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c index b4c5c6ea06..9410e670d0 100644 --- a/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c +++ b/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c @@ -19,11 +19,7 @@ * KIND, either express or implied. * ****************************************************************************/ - #include "config.h" - -#ifdef HAVE_HW_H264 - #include "system.h" #include "kernel.h" #include "s5l87xx.h" @@ -1077,5 +1073,3 @@ const struct hw_h264_api target_hw_h264_api = { .get_frame = vpu_h264_get_frame, .close = vpu_h264_close, }; - -#endif /* HAVE_HW_H264 */ From 190822f2611cc6759ccbcfb9ff76fc92c6502482 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Tue, 8 Sep 2026 10:59:00 +0100 Subject: [PATCH 213/408] firmware: limit system_memory_guard() to coldfire targets Only Coldfire targets have ever implemented this. Gate it behind CPU_COLDFIRE so the stub functions won't be needed in other targets. Change-Id: I507952c40a04d813a40296142a6eba1df24b0a68 --- apps/plugin.c | 25 ++++++++-------- apps/plugin.h | 30 +++++++++---------- docs/PLUGIN_API | 2 +- firmware/target/arm/as3525/system-as3525.c | 6 ---- firmware/target/arm/imx233/system-imx233.c | 6 ---- .../arm/imx31/gigabeat-s/system-gigabeat-s.c | 6 ---- firmware/target/arm/pp/system-pp5002.c | 6 ---- firmware/target/arm/pp/system-pp502x.c | 6 ---- firmware/target/arm/rk27xx/system-rk27xx.c | 6 ---- firmware/target/arm/s3c2440/system-s3c2440.c | 6 ---- firmware/target/arm/s5l8700/system-s5l8700.c | 6 ---- firmware/target/arm/s5l8702/system-s5l8702.c | 6 ---- firmware/target/arm/stm32/system-stm32h7.c | 7 ----- firmware/target/arm/tcc780x/system-tcc780x.c | 6 ---- .../target/arm/tms320dm320/system-dm320.c | 6 ---- .../mips/ingenic_jz47xx/system-jz4740.c | 6 ---- .../mips/ingenic_jz47xx/system-jz4760.c | 6 ---- .../target/mips/ingenic_x1000/system-x1000.c | 7 ----- 18 files changed, 27 insertions(+), 122 deletions(-) diff --git a/apps/plugin.c b/apps/plugin.c index ec42d25a61..b62f6f3107 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -263,6 +263,7 @@ static const struct plugin_api rockbox_api = { font_load, font_unload, font_get, + font_measurestring, font_getstringsize, font_get_width, screen_clear_area, @@ -319,6 +320,7 @@ static const struct plugin_api rockbox_api = { is_backlight_on, backlight_on, backlight_off, + backlight_set_on_button_hold, backlight_set_timeout, #ifdef HAVE_BACKLIGHT_BRIGHTNESS backlight_set_brightness, @@ -335,6 +337,9 @@ static const struct plugin_api rockbox_api = { #if CONFIG_CHARGING remote_backlight_set_timeout_plugged, #endif +#if defined(HAS_REMOTE_BUTTON_HOLD) + remote_backlight_set_on_button_hold, +#endif #endif /* HAVE_REMOTE_LCD */ #endif /* HAVE_BACKLIGHT */ @@ -381,6 +386,8 @@ static const struct plugin_api rockbox_api = { gesture_vel_reset, gesture_vel_process, gesture_vel_get, + gesture_flick_get_in_vp, + gesture_flick_get, #endif action_userabort, core_set_keyremap, @@ -517,7 +524,9 @@ static const struct plugin_api rockbox_api = { set_sleeptimer_duration, /*stub*/ get_sleep_timer, /*stub*/ #if (CONFIG_PLATFORM & PLATFORM_NATIVE) +#if defined(CPU_COLDFIRE) system_memory_guard, +#endif &cpu_frequency, #ifdef HAVE_ADJUSTABLE_CPU_FREQ @@ -873,22 +882,12 @@ static const struct plugin_api rockbox_api = { path_strip_volume, #endif - /* new stuff at the end, sort into place next time - the API gets incompatible */ - font_measurestring, -#ifdef HAVE_TOUCHSCREEN - gesture_flick_get_in_vp, - gesture_flick_get, -#endif #ifdef HAVE_HW_H264 &target_hw_h264_api, #endif -#ifdef HAVE_BACKLIGHT - backlight_set_on_button_hold, -#endif -#if defined(HAVE_REMOTE_LCD) && defined(HAS_REMOTE_BUTTON_HOLD) - remote_backlight_set_on_button_hold, -#endif + + /* new stuff at the end, sort into place next time + the API gets incompatible */ }; static int plugin_buffer_handle; diff --git a/apps/plugin.h b/apps/plugin.h index ab60404a3b..54fedf28ec 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -307,6 +307,8 @@ struct plugin_api { int (*font_load)(const char *path); void (*font_unload)(int font_id); struct font* (*font_get)(int font); + int (*font_measurestring)(const unsigned char *str, size_t maxbytes, + size_t maxwidth, int *w, int *h, int fontnum); int (*font_getstringsize)(const unsigned char *str, int *w, int *h, int fontnumber); int (*font_get_width)(struct font* pf, ucschar_t char_code); @@ -374,11 +376,11 @@ struct plugin_api { bool (*is_backlight_on)(bool ignore_always_off); void (*backlight_on)(void); void (*backlight_off)(void); + void (*backlight_set_on_button_hold)(int index); void (*backlight_set_timeout)(int index); #ifdef HAVE_BACKLIGHT_BRIGHTNESS void (*backlight_set_brightness)(int val); #endif /* HAVE_BACKLIGHT_BRIGHTNESS */ - #if CONFIG_CHARGING void (*backlight_set_timeout_plugged)(int index); #endif @@ -390,6 +392,9 @@ struct plugin_api { #if CONFIG_CHARGING void (*remote_backlight_set_timeout_plugged)(int index); #endif +#if defined(HAS_REMOTE_BUTTON_HOLD) + void (*remote_backlight_set_on_button_hold)(int index); +#endif #endif /* HAVE_REMOTE_LCD */ #endif /* HAVE_BACKLIGHT */ @@ -451,6 +456,9 @@ struct plugin_api { void (*gesture_vel_process)(struct gesture_vel *gv, const struct touchevent *ev); bool (*gesture_vel_get)(struct gesture_vel *gv, int *xvel, int *yvel); + int (*gesture_flick_get_in_vp)(const struct gesture_event *gevt, + const struct viewport *vp); + int (*gesture_flick_get)(const struct gesture_event *gevt); #endif bool (*action_userabort)(int timeout); int (*core_set_keyremap)(struct button_mapping* core_keymap, int count); @@ -603,7 +611,9 @@ struct plugin_api { void (*set_sleeptimer_duration)(int minutes); int (*get_sleep_timer)(void); #if (CONFIG_PLATFORM & PLATFORM_NATIVE) +#if defined(CPU_COLDFIRE) int (*system_memory_guard)(int newmode); +#endif long *cpu_frequency; #ifdef HAVE_ADJUSTABLE_CPU_FREQ #ifdef CPU_BOOST_LOGGING @@ -1038,24 +1048,12 @@ struct plugin_api { int (*path_strip_volume)(const char *name, const char **nameptr, bool greedy); #endif - /* new stuff at the end, sort into place next time - the API gets incompatible */ - int (*font_measurestring)(const unsigned char *str, size_t maxbytes, - size_t maxwidth, int *w, int *h, int fontnum); -#ifdef HAVE_TOUCHSCREEN - int (*gesture_flick_get_in_vp)(const struct gesture_event *gevt, - const struct viewport *vp); - int (*gesture_flick_get)(const struct gesture_event *gevt); -#endif #ifdef HAVE_HW_H264 const struct hw_h264_api *hw_h264; #endif -#ifdef HAVE_BACKLIGHT - void (*backlight_set_on_button_hold)(int index); -#endif -#if defined(HAVE_REMOTE_LCD) && defined(HAS_REMOTE_BUTTON_HOLD) - void (*remote_backlight_set_on_button_hold)(int index); -#endif + + /* new stuff at the end, sort into place next time + the API gets incompatible */ }; /* plugin header */ diff --git a/docs/PLUGIN_API b/docs/PLUGIN_API index afe701efe2..38a584034a 100644 --- a/docs/PLUGIN_API +++ b/docs/PLUGIN_API @@ -2663,7 +2663,7 @@ char* strtok_r(char *ptr, const char *sep, char **end) int system_memory_guard(int newmode) \group kernel/ system - \conditions ((CONFIG_PLATFORM & PLATFORM_NATIVE)) + \conditions ((CONFIG_PLATFORM & PLATFORM_NATIVE) && defined(CPU_COLDFIRE)) \param newmode \return \description diff --git a/firmware/target/arm/as3525/system-as3525.c b/firmware/target/arm/as3525/system-as3525.c index aced3e2b80..c57544d7aa 100644 --- a/firmware/target/arm/as3525/system-as3525.c +++ b/firmware/target/arm/as3525/system-as3525.c @@ -398,12 +398,6 @@ void system_exception_wait(void) while(!button_read_device()); } -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} - /* usecs may be at most 2^32/248 (17 seconds) for 248MHz max cpu freq */ void udelay(unsigned usecs) { diff --git a/firmware/target/arm/imx233/system-imx233.c b/firmware/target/arm/imx233/system-imx233.c index 0bb62639d0..3b79aa079c 100644 --- a/firmware/target/arm/imx233/system-imx233.c +++ b/firmware/target/arm/imx233/system-imx233.c @@ -154,12 +154,6 @@ void system_exception_wait(void) while(imx233_power_read_pswitch() != 0) {} } -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} - static void set_page_tables(void) { /* map every memory region to itself */ diff --git a/firmware/target/arm/imx31/gigabeat-s/system-gigabeat-s.c b/firmware/target/arm/imx31/gigabeat-s/system-gigabeat-s.c index 4ae9439134..e3a4c722d0 100644 --- a/firmware/target/arm/imx31/gigabeat-s/system-gigabeat-s.c +++ b/firmware/target/arm/imx31/gigabeat-s/system-gigabeat-s.c @@ -174,12 +174,6 @@ void gpt_stop(void) restore_interrupt(status); } -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} - void system_halt(void) { disable_interrupt(IRQ_FIQ_STATUS); diff --git a/firmware/target/arm/pp/system-pp5002.c b/firmware/target/arm/pp/system-pp5002.c index 75f99674d7..1cf284c294 100644 --- a/firmware/target/arm/pp/system-pp5002.c +++ b/firmware/target/arm/pp/system-pp5002.c @@ -235,9 +235,3 @@ void system_exception_wait(void) sleep_core(CURRENT_CORE); while (1); } - -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} diff --git a/firmware/target/arm/pp/system-pp502x.c b/firmware/target/arm/pp/system-pp502x.c index e8834061e1..ef69a459ec 100644 --- a/firmware/target/arm/pp/system-pp502x.c +++ b/firmware/target/arm/pp/system-pp502x.c @@ -664,9 +664,3 @@ void system_exception_wait(void) PROC_CTL(CURRENT_CORE) = 0x40000000; while (1); } - -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} diff --git a/firmware/target/arm/rk27xx/system-rk27xx.c b/firmware/target/arm/rk27xx/system-rk27xx.c index 0d71915a00..6b88d86840 100644 --- a/firmware/target/arm/rk27xx/system-rk27xx.c +++ b/firmware/target/arm/rk27xx/system-rk27xx.c @@ -173,12 +173,6 @@ void system_exception_wait(void) while(!button_read_device()); } -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} - /* usecs may be at most 2^32/200 (~21 seconds) for 200MHz max cpu freq */ void udelay(unsigned usecs) { diff --git a/firmware/target/arm/s3c2440/system-s3c2440.c b/firmware/target/arm/s3c2440/system-s3c2440.c index 938ee0276a..060065e20d 100644 --- a/firmware/target/arm/s3c2440/system-s3c2440.c +++ b/firmware/target/arm/s3c2440/system-s3c2440.c @@ -195,12 +195,6 @@ void system_init(void) #endif } -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} - #ifdef HAVE_ADJUSTABLE_CPU_FREQ void set_cpu_frequency(long frequency) diff --git a/firmware/target/arm/s5l8700/system-s5l8700.c b/firmware/target/arm/s5l8700/system-s5l8700.c index eb9dd09881..81d574bfef 100644 --- a/firmware/target/arm/s5l8700/system-s5l8700.c +++ b/firmware/target/arm/s5l8700/system-s5l8700.c @@ -207,12 +207,6 @@ void system_exception_wait(void) while(1); } -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} - #ifdef HAVE_ADJUSTABLE_CPU_FREQ void set_cpu_frequency(long frequency) diff --git a/firmware/target/arm/s5l8702/system-s5l8702.c b/firmware/target/arm/s5l8702/system-s5l8702.c index 8b6a314ce9..0a6548dd8d 100644 --- a/firmware/target/arm/s5l8702/system-s5l8702.c +++ b/firmware/target/arm/s5l8702/system-s5l8702.c @@ -267,12 +267,6 @@ void system_exception_wait(void) while(1); } -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} - #ifdef HAVE_ADJUSTABLE_CPU_FREQ void set_cpu_frequency(long frequency) { diff --git a/firmware/target/arm/stm32/system-stm32h7.c b/firmware/target/arm/stm32/system-stm32h7.c index c1c311c18e..cbc731de4c 100644 --- a/firmware/target/arm/stm32/system-stm32h7.c +++ b/firmware/target/arm/stm32/system-stm32h7.c @@ -156,10 +156,3 @@ void udelay(uint32_t us) start = value; } } - -int system_memory_guard(int newmode) -{ - /* TODO -- maybe use MPU here to give some basic protection */ - (void)newmode; - return MEMGUARD_NONE; -} diff --git a/firmware/target/arm/tcc780x/system-tcc780x.c b/firmware/target/arm/tcc780x/system-tcc780x.c index 55b87f1db3..c049b308c6 100644 --- a/firmware/target/arm/tcc780x/system-tcc780x.c +++ b/firmware/target/arm/tcc780x/system-tcc780x.c @@ -294,12 +294,6 @@ void system_exception_wait(void) #endif } -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} - #ifdef HAVE_ADJUSTABLE_CPU_FREQ void set_cpu_frequency(long frequency) diff --git a/firmware/target/arm/tms320dm320/system-dm320.c b/firmware/target/arm/tms320dm320/system-dm320.c index edbe14fc33..b47aa4ac16 100644 --- a/firmware/target/arm/tms320dm320/system-dm320.c +++ b/firmware/target/arm/tms320dm320/system-dm320.c @@ -393,12 +393,6 @@ void system_init(void) #endif } -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} - #ifdef HAVE_ADJUSTABLE_CPU_FREQ void set_cpu_frequency(long frequency) { diff --git a/firmware/target/mips/ingenic_jz47xx/system-jz4740.c b/firmware/target/mips/ingenic_jz47xx/system-jz4740.c index bf0e46138b..a854f6bf8c 100644 --- a/firmware/target/mips/ingenic_jz47xx/system-jz4740.c +++ b/firmware/target/mips/ingenic_jz47xx/system-jz4740.c @@ -500,12 +500,6 @@ void system_init(void) { } -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} - #ifdef HAVE_ADJUSTABLE_CPU_FREQ void set_cpu_frequency(long frequency) { diff --git a/firmware/target/mips/ingenic_jz47xx/system-jz4760.c b/firmware/target/mips/ingenic_jz47xx/system-jz4760.c index 7d2a5c7109..aa2e8330fd 100644 --- a/firmware/target/mips/ingenic_jz47xx/system-jz4760.c +++ b/firmware/target/mips/ingenic_jz47xx/system-jz4760.c @@ -733,12 +733,6 @@ void system_init(void) { } -int system_memory_guard(int newmode) -{ - (void)newmode; - return 0; -} - #ifdef HAVE_ADJUSTABLE_CPU_FREQ void set_cpu_frequency(long frequency) { diff --git a/firmware/target/mips/ingenic_x1000/system-x1000.c b/firmware/target/mips/ingenic_x1000/system-x1000.c index ec94c7236f..36f2a9fc9a 100644 --- a/firmware/target/mips/ingenic_x1000/system-x1000.c +++ b/firmware/target/mips/ingenic_x1000/system-x1000.c @@ -196,13 +196,6 @@ void system_reboot(void) while(1); } -int system_memory_guard(int mode) -{ - /* unused */ - (void)mode; - return 0; -} - /* Simple delay API -- slow path functions */ void __udelay(uint32_t us) From c6abf3382a67aa5dcb3b85abe01ad4cfbdcd717d Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Tue, 8 Sep 2026 11:22:42 +0100 Subject: [PATCH 214/408] firmware: move iriver flash helper functions into target tree Move the iriver-specific functions for detecting flashed Rockbox/OF images into system-iriver.c and remove the HAVE_FLASHED_ROCKBOX define which is now redundant (all targets using system-iriver.c enable it). Copyright attribution on the new system-iriver.h header is a best guess from Git history. Change-Id: If1933f881a63fd517162ab9ca8f4a3007b997739 --- apps/plugin.c | 4 ++ apps/plugins/iriver_flash.c | 1 + bootloader/iriver_h300.c | 1 + firmware/eeprom_settings.c | 1 + firmware/export/config/iriverh100.h | 4 -- firmware/export/config/iriverh120.h | 3 -- firmware/export/config/iriverh300.h | 3 -- firmware/export/config/sim.h | 2 - firmware/export/system.h | 10 ----- firmware/system.c | 40 ------------------- .../target/coldfire/iriver/system-iriver.c | 30 ++++++++++++++ .../target/coldfire/iriver/system-iriver.h | 37 +++++++++++++++++ 12 files changed, 74 insertions(+), 62 deletions(-) create mode 100644 firmware/target/coldfire/iriver/system-iriver.h diff --git a/apps/plugin.c b/apps/plugin.c index b62f6f3107..bdcdffbdcc 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -67,6 +67,10 @@ #include "usbstack/usb_audio.h" #endif +#if defined(IRIVER_H100_SERIES) || defined(IRIVER_H300_SERIES) +#include "system-iriver.h" +#endif + #define WRAPPER(_x_) _x_ ## _wrapper #if (CONFIG_PLATFORM & PLATFORM_HOSTED) diff --git a/apps/plugins/iriver_flash.c b/apps/plugins/iriver_flash.c index 1ad222bf40..2b6f70adf9 100644 --- a/apps/plugins/iriver_flash.c +++ b/apps/plugins/iriver_flash.c @@ -23,6 +23,7 @@ #include "plugin.h" #include "lib/helper.h" #include "checksum.h" +#include "system-iriver.h" /* * Flash commands may rely on null pointer dereferences to work correctly. diff --git a/bootloader/iriver_h300.c b/bootloader/iriver_h300.c index 98f72e14ca..8fb273edb2 100644 --- a/bootloader/iriver_h300.c +++ b/bootloader/iriver_h300.c @@ -25,6 +25,7 @@ #include "string.h" #include "cpu.h" #include "system.h" +#include "system-iriver.h" #include "lcd.h" #include "lcd-remote.h" #include "scroll_engine.h" diff --git a/firmware/eeprom_settings.c b/firmware/eeprom_settings.c index b2e75ebf54..abd719b89a 100644 --- a/firmware/eeprom_settings.c +++ b/firmware/eeprom_settings.c @@ -23,6 +23,7 @@ #include "crc32.h" #include "system.h" +#include "system-iriver.h" #include "string.h" #include "logf.h" diff --git a/firmware/export/config/iriverh100.h b/firmware/export/config/iriverh100.h index e8add732de..5d9b46fec9 100644 --- a/firmware/export/config/iriverh100.h +++ b/firmware/export/config/iriverh100.h @@ -141,10 +141,6 @@ /* Define this if you want to use coldfire's i2c interface */ #define CONFIG_I2C I2C_COLDFIRE -/* Define this if you can run rockbox from flash memory */ -/* In theory we can, but somebody needs to verify there are no issues. */ -#define HAVE_FLASHED_ROCKBOX - /* define this if the hardware can be powered off while charging */ #define HAVE_POWEROFF_WHILE_CHARGING diff --git a/firmware/export/config/iriverh120.h b/firmware/export/config/iriverh120.h index b591b10e83..00fba08361 100644 --- a/firmware/export/config/iriverh120.h +++ b/firmware/export/config/iriverh120.h @@ -148,9 +148,6 @@ /* Define this if you want to use coldfire's i2c interface */ #define CONFIG_I2C I2C_COLDFIRE -/* Define this if you can run rockbox from flash memory */ -#define HAVE_FLASHED_ROCKBOX - /* define this if the hardware can be powered off while charging */ #define HAVE_POWEROFF_WHILE_CHARGING diff --git a/firmware/export/config/iriverh300.h b/firmware/export/config/iriverh300.h index 4b93c62cec..572210e8f7 100644 --- a/firmware/export/config/iriverh300.h +++ b/firmware/export/config/iriverh300.h @@ -157,9 +157,6 @@ /* Define this if you want to use coldfire's i2c interface */ #define CONFIG_I2C I2C_COLDFIRE -/* Define this if you can run rockbox from flash memory */ -#define HAVE_FLASHED_ROCKBOX - /* The size of the flash ROM */ #define FLASH_SIZE 0x400000 diff --git a/firmware/export/config/sim.h b/firmware/export/config/sim.h index b4ba80a87a..fe9194da35 100644 --- a/firmware/export/config/sim.h +++ b/firmware/export/config/sim.h @@ -77,8 +77,6 @@ #undef INCLUDE_TIMEOUT_API -#undef HAVE_FLASHED_ROCKBOX - #undef IPOD_ACCESSORY_PROTOCOL #undef HAVE_WHEEL_POSITION diff --git a/firmware/export/system.h b/firmware/export/system.h index 498c0b048d..cdfa948836 100644 --- a/firmware/export/system.h +++ b/firmware/export/system.h @@ -40,16 +40,6 @@ extern void system_init(void); extern long cpu_frequency; -struct flash_header { - uint32_t magic; - uint32_t length; - char version[32]; -}; - -bool detect_flashed_romimage(void); -bool detect_flashed_ramimage(void); -bool detect_original_firmware(void); - #if defined(HAVE_ADJUSTABLE_CPU_FREQ) \ && defined(ROCKBOX_HAS_LOGF) && (NUM_CORES == 1) #define CPU_BOOST_LOGGING diff --git a/firmware/system.c b/firmware/system.c index 20c927efeb..d709bb1a02 100644 --- a/firmware/system.c +++ b/firmware/system.c @@ -161,43 +161,3 @@ void cpu_idle_mode(bool on_off) cpu_boost_unlock(); } #endif /* HAVE_ADJUSTABLE_CPU_FREQ */ - - -#ifdef HAVE_FLASHED_ROCKBOX -static bool detect_flash_header(uint8_t *addr) -{ -#ifndef BOOTLOADER - int oldmode = system_memory_guard(MEMGUARD_NONE); -#endif - struct flash_header hdr; - memcpy(&hdr, addr, sizeof(struct flash_header)); -#ifndef BOOTLOADER - system_memory_guard(oldmode); -#endif - return hdr.magic == FLASH_MAGIC; -} -#endif - -bool detect_flashed_romimage(void) -{ -#ifdef HAVE_FLASHED_ROCKBOX - return detect_flash_header((uint8_t *)FLASH_ROMIMAGE_ENTRY); -#else - return false; -#endif /* HAVE_FLASHED_ROCKBOX */ -} - -bool detect_flashed_ramimage(void) -{ -#ifdef HAVE_FLASHED_ROCKBOX - return detect_flash_header((uint8_t *)FLASH_RAMIMAGE_ENTRY); -#else - return false; -#endif /* HAVE_FLASHED_ROCKBOX */ -} - -bool detect_original_firmware(void) -{ - return !(detect_flashed_ramimage() || detect_flashed_romimage()); -} - diff --git a/firmware/target/coldfire/iriver/system-iriver.c b/firmware/target/coldfire/iriver/system-iriver.c index d16931ac1f..da709e5c38 100644 --- a/firmware/target/coldfire/iriver/system-iriver.c +++ b/firmware/target/coldfire/iriver/system-iriver.c @@ -21,9 +21,11 @@ #include "cpu.h" #include "kernel.h" #include "system.h" +#include "system-iriver.h" #include "power.h" #include "timer.h" #include "pcf50606.h" +#include /* Settings for all possible clock frequencies (with properly working timers) * NOTE: Some 5249 chips don't like having PLLDIV set to 0. We must avoid that! @@ -161,3 +163,31 @@ void cf_set_cpu_frequency(long frequency) break; } } + +static bool detect_flash_header(uint8_t *addr) +{ +#ifndef BOOTLOADER + int oldmode = system_memory_guard(MEMGUARD_NONE); +#endif + struct flash_header hdr; + memcpy(&hdr, addr, sizeof(struct flash_header)); +#ifndef BOOTLOADER + system_memory_guard(oldmode); +#endif + return hdr.magic == FLASH_MAGIC; +} + +bool detect_flashed_romimage(void) +{ + return detect_flash_header((uint8_t *)FLASH_ROMIMAGE_ENTRY); +} + +bool detect_flashed_ramimage(void) +{ + return detect_flash_header((uint8_t *)FLASH_RAMIMAGE_ENTRY); +} + +bool detect_original_firmware(void) +{ + return !(detect_flashed_ramimage() || detect_flashed_romimage()); +} diff --git a/firmware/target/coldfire/iriver/system-iriver.h b/firmware/target/coldfire/iriver/system-iriver.h new file mode 100644 index 0000000000..e87a108c35 --- /dev/null +++ b/firmware/target/coldfire/iriver/system-iriver.h @@ -0,0 +1,37 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2007 Miika Pekkarinen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef __SYSTEM_IRIVER_H__ +#define __SYSTEM_IRIVER_H__ + +#include +#include + +struct flash_header +{ + uint32_t magic; + uint32_t length; + char version[32]; +}; + +bool detect_flashed_romimage(void); +bool detect_flashed_ramimage(void); +bool detect_original_firmware(void); + +#endif /* __SYSTEM_IRIVER_H__ */ From 4e4198af7a2e95d640e5493c3449d96a9880e59f Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Wed, 23 Sep 2026 14:16:41 -0400 Subject: [PATCH 215/408] fix red in c6abf3382a -- iriver h1xx bootloader Change-Id: Id81dc1e89941ddc1102e60138907c0d07fa9f538 --- bootloader/iriver_h1x0.c | 1 + 1 file changed, 1 insertion(+) diff --git a/bootloader/iriver_h1x0.c b/bootloader/iriver_h1x0.c index 68c00390a4..380d0e91bd 100644 --- a/bootloader/iriver_h1x0.c +++ b/bootloader/iriver_h1x0.c @@ -25,6 +25,7 @@ #include "string.h" #include "cpu.h" #include "system.h" +#include "system-iriver.h" #include "lcd.h" #include "lcd-remote.h" #include "scroll_engine.h" From 5448dd99a337cafe66901eae1d0b481f6c42b75c Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Wed, 23 Sep 2026 14:17:05 -0400 Subject: [PATCH 216/408] fix red in c6abf3382a -- iriver h1xx/h3xx simulator Change-Id: I2b6f207aa8b349ca9cf12f8f9334848a65c4f48a --- apps/plugin.c | 2 +- apps/plugin.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/plugin.c b/apps/plugin.c index bdcdffbdcc..dfb1cbdb82 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -860,7 +860,7 @@ static const struct plugin_api rockbox_api = { wheel_send_events, #endif -#if defined(IRIVER_H100_SERIES) || defined(IRIVER_H300_SERIES) +#if !defined(SIMULATOR) && (defined(IRIVER_H100_SERIES) || defined(IRIVER_H300_SERIES)) /* Routines for the iriver_flash -plugin. */ detect_original_firmware, detect_flashed_ramimage, diff --git a/apps/plugin.h b/apps/plugin.h index 54fedf28ec..26432b5b9b 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -1025,7 +1025,7 @@ struct plugin_api { void (*wheel_send_events)(bool send); #endif -#if defined(IRIVER_H100_SERIES) || defined(IRIVER_H300_SERIES) +#if !defined(SIMULATOR) && (defined(IRIVER_H100_SERIES) || defined(IRIVER_H300_SERIES)) /* Routines for the iriver_flash -plugin. */ bool (*detect_original_firmware)(void); bool (*detect_flashed_ramimage)(void); From 9101f3551965245662b89a9e54742282d52f8f11 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Wed, 23 Sep 2026 16:48:29 -0400 Subject: [PATCH 217/408] ROLO: Get rid of redundant call to audio_hard_stop() audio_hard_stop() should be called *prior* to rolo_load(), and indeed already is at every call site. The reason to remove it from inside rolo_load() as opposed to the call sites is because those already show a feedback splash while the audio path is being shut down. Change-Id: Ib6b995f7172c6a92599ace75245909730b2e941a --- firmware/rolo.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/firmware/rolo.c b/firmware/rolo.c index d832496270..0210cfe78c 100644 --- a/firmware/rolo.c +++ b/firmware/rolo.c @@ -28,7 +28,6 @@ #include "kernel.h" #include "button.h" #include "file.h" -#include "audio.h" #include "system.h" #include "i2c.h" #include "adc.h" @@ -219,6 +218,7 @@ extern unsigned long loadaddress; * Name: rolo_load(const char *filename) * Filename must be a fully defined filename including the path and extension * + * Must call audio_hard_stop() before calling us! ***************************************************************************/ int rolo_load(const char* filename) { @@ -238,8 +238,6 @@ int rolo_load(const char* filename) lcd_remote_update(); #endif - audio_hard_stop(); - /* get the system buffer. release only in case of error, otherwise * we don't return anyway */ rolo_handle = core_alloc_maximum(&filebuf_size, &buflib_ops_locked); From d1fab121f84c49b1b7183e9d58fc13963d407f9f Mon Sep 17 00:00:00 2001 From: Paul Sauro Date: Sun, 20 Sep 2026 17:21:42 +0200 Subject: [PATCH 218/408] usb arc: flush both EP0 directions when SETUP replaces a transfer The USB core can accept a replacement SETUP while the previous control transfer is unfinished. Before dispatching that SETUP, flush both EP0 directions and discard their old completion bits so old descriptors cannot be reported as the new transfer. Leave non-control endpoints alone. The register-stub test verifies both flushes, EP0 completion clearing and release of old EP0 waiters. This complements the separate generic control-request state-machine fix. Change-Id: If595c7c0cfade7d855f113587621847926143606 --- firmware/target/arm/usb-drv-arc.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/firmware/target/arm/usb-drv-arc.c b/firmware/target/arm/usb-drv-arc.c index 20bd895f65..7f3c7758fe 100644 --- a/firmware/target/arm/usb-drv-arc.c +++ b/firmware/target/arm/usb-drv-arc.c @@ -362,7 +362,7 @@ static const unsigned int pipe2mask[USB_NUM_ENDPOINTS*2] = { /*-------------------------------------------------------------------------*/ static void transfer_completed(void); -static void control_received(void); +static void setup_received(void); static void sof_received(void); static int prime_transfer(int ep_num, void* ptr, int len, bool send, bool wait); static void prepare_td(struct transfer_descriptor* td, @@ -575,7 +575,7 @@ void usb_drv_int(void) /* a control packet? */ if (REG_ENDPTSETUPSTAT & EPSETUP_STATUS_EP0) { - control_received(); + setup_received(); } if (REG_ENDPTCOMPLETE) @@ -1070,7 +1070,7 @@ static void prepare_td(struct transfer_descriptor* td, } } -static void control_received(void) +static void setup_received(void) { int i; /* copy setup data from packet */ @@ -1081,6 +1081,13 @@ static void control_received(void) /* acknowledge packet recieved */ REG_ENDPTSETUPSTAT = EPSETUP_STATUS_EP0; + /* A new SETUP cancels both halves of the old control transfer. Flush + * only EP0: cancelling storage/HID here would lose unrelated traffic. + * Discard its completion bits before the core primes the new request. */ + usb_drv_reset_endpoint(EP_CONTROL, false); + usb_drv_reset_endpoint(EP_CONTROL, true); + REG_ENDPTCOMPLETE = pipe2mask[0] | pipe2mask[1]; + /* Stop pending control transfers */ for(i=0;i<2;i++) { if(qh_array[i].wait) { From 31f2a27f1e0908b584730a62990eb178093cfca2 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:22 -0400 Subject: [PATCH 219/408] opus: inline EC_ILOG on ARMv4 ARMv4 has no CLZ, so all 8,680 ilog2 calls went through libgcc's __clzsi2. Fifteen branchless instructions replace it. Modelled: -0.91% ARMv4; ARMv5E unaffected, it already emits CLZ. Co-Authored-By: Claude Opus 5 Change-Id: Ib84b97acb687da4101e02bad1b0e8bbd256d32df --- lib/rbcodec/codecs/libopus/celt/ecintrin.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/rbcodec/codecs/libopus/celt/ecintrin.h b/lib/rbcodec/codecs/libopus/celt/ecintrin.h index 2263cff6bd..4336c4dec7 100644 --- a/lib/rbcodec/codecs/libopus/celt/ecintrin.h +++ b/lib/rbcodec/codecs/libopus/celt/ecintrin.h @@ -64,6 +64,9 @@ static __inline int ec_bsr(unsigned long _x){ # include "dsplib.h" # define EC_CLZ0 (31) # define EC_CLZ(_x) (_lnorm(_x)) +#elif defined(OPUS_ARM_INLINE_ASM) && !defined(OPUS_ARM_NO_ILOG_INLINE) +/*ARMv4 has no CLZ instruction, so __builtin_clz compiles to a call to + libgcc's __clzsi2. Leave EC_CLZ undefined and inline EC_ILOG below.*/ #elif __GNUC_PREREQ(3,4) # if INT_MAX>=2147483647 # define EC_CLZ0 ((int)sizeof(unsigned)*CHAR_BIT) @@ -80,6 +83,23 @@ static __inline int ec_bsr(unsigned long _x){ The majority of the time we can never pass it zero. When we need to, it can be special cased.*/ # define EC_ILOG(_x) (EC_CLZ0-EC_CLZ(_x)) +#elif defined(OPUS_ARM_INLINE_ASM) && !defined(OPUS_ARM_NO_ILOG_INLINE) +/*Fifteen branchless instructions, no memory access. The libgcc call it + replaces costs about half again once the call, its table load and the + caller's register saves are counted. Compares against 0x10000, 0x100 and + 0x10 because those are ARM immediates and the equivalent masks are not. + Returns 0 for 0, where the builtin form is undefined.*/ +static __inline__ int ec_ilog_armv4(opus_uint32 _v){ + int ret; + ret=0; + if(_v>=0x10000){_v>>=16;ret=16;} + if(_v>=0x100){_v>>=8;ret+=8;} + if(_v>=0x10){_v>>=4;ret+=4;} + if(_v>=0x4){_v>>=2;ret+=2;} + /*_v is now 0..3, whose ilog is 0, 1, 2, 2.*/ + return ret+(int)(_v&~(_v>>1)); +} +# define EC_ILOG(_x) (ec_ilog_armv4(_x)) #else int ec_ilog(opus_uint32 _v); # define EC_ILOG(_x) (ec_ilog(_x)) From af2a3b74f49e25446aeeeddb613ba53d85996532 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Tue, 22 Sep 2026 23:24:46 +0300 Subject: [PATCH 220/408] ipodnano3g: Show a QR code if the NAND is not supported Currently, the link points to the unofficial bootloader builds and installation instructions by freemyipod.org, but it can be changed at any time by the rockbox.org website maintainer. Co-authored-by: ChatGPT-5.6 Luna Change-Id: I05a78a6540ebcc0e03a1f12f5d765e69e68af00c --- bootloader/ipod-s5l87xx.c | 74 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/bootloader/ipod-s5l87xx.c b/bootloader/ipod-s5l87xx.c index c484ae97d8..2ff2206a57 100644 --- a/bootloader/ipod-s5l87xx.c +++ b/bootloader/ipod-s5l87xx.c @@ -887,6 +887,74 @@ static void devel_menu(void) } #endif /* S5L87XX_DEVELOPMENT_BOOTLOADER */ +static void lcd_qr_code(const char *data, unsigned int row_stride, + int x, int y, unsigned int qr_cols, unsigned int qr_rows, + unsigned int modules_scale, unsigned int margin_thickness) +{ + const unsigned int margin_size = margin_thickness * 2; + const unsigned int width = qr_cols * modules_scale + margin_size; + const unsigned int height = qr_rows * modules_scale + margin_size; + + // -1 = align right, -2 = align center + const unsigned int x0 = x >= 0 ? (unsigned int)x : (LCD_WIDTH - width) / abs(x); + const unsigned int y0 = y >= 0 ? (unsigned int)y : (LCD_HEIGHT - height) / abs(y); + + lcd_set_foreground(LCD_WHITE); + lcd_fillrect(x0, y0, width, height); + + lcd_set_foreground(LCD_BLACK); + for (unsigned int row = 0; row < qr_rows; row++) { + for (unsigned int col = 0; col < qr_cols; col++) { + if (data[row * row_stride + col] != ' ') { + lcd_fillrect( + x0 + margin_thickness + col * modules_scale, + y0 + margin_thickness + row * modules_scale, + modules_scale, + modules_scale + ); + } + } + } + + lcd_update(); +} + +static const char qr_code_data[][22] = { + /* + * qrencode -o - -t ascii -s 1 -m 0 -i 'https://rockbox.org/IPNBL' | \ + * cut -c 1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41 | \ + * awk '{ print "\"" $0 "\"," }' + */ + + "####### # #######", + "# # ## # # #", + "# ### # ## # # ### #", + "# ### # # # # # ### #", + "# ### # # ## # ### #", + "# # #### # #", + "####### # # # #######", + " ## ", + "##### ######## # # # ", + "# # #### # # #", + " # # # #", + "## # # # # ## #### ", + "# ### ## ### ", + " ## # # ####", + "####### ## # ### #", + "# # ### # ## ", + "# ### # # #### ###", + "# ### # ### ### ## ", + "# ### # #### # # ", + "# # # ## ## # #", + "####### # ## ## ", +}; + +enum { + QR_COLS = sizeof(qr_code_data[0]) - 1, + QR_ROWS = sizeof(qr_code_data) / sizeof(qr_code_data[0]), + QR_STRIDE = sizeof(qr_code_data[0]), +}; + void main(void) { int rc = 0; @@ -1013,8 +1081,10 @@ void main(void) printf("Rockbox does not write to"); printf("chips it has not been"); printf("tested on."); - printf("Starting Apple firmware..."); - sleep(8 * HZ); /* long enough to read the message */ + printf("Starting Apple firmware in 60s..."); + lcd_qr_code(&qr_code_data[0][0], QR_STRIDE, -2, LCD_HEIGHT / 2, QR_COLS, QR_ROWS, 4, 8); + sleep(60 * HZ); /* long enough to read the message and scan the QR code */ + lcd_set_foreground(LCD_WHITE); rc = kernel_launch_onb(); /* Only reached if the ONB could not be read from NOR */ printf("Apple firmware failed: %d", rc); From bdbbb753c58304b4d6ce61048cfb08e21d0a00b9 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Thu, 24 Sep 2026 15:44:56 -0400 Subject: [PATCH 221/408] ipod6gboot: Silence build warning introduced in af2a3b74f4 (ipod6g doesn't utilize the QR Code stuff used by the nano3g) Change-Id: I99c9e565b5a4c7212662eab097f1bae2f4ed0ea4 --- bootloader/ipod-s5l87xx.c | 68 ++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/bootloader/ipod-s5l87xx.c b/bootloader/ipod-s5l87xx.c index 2ff2206a57..ef97a7dfb6 100644 --- a/bootloader/ipod-s5l87xx.c +++ b/bootloader/ipod-s5l87xx.c @@ -65,6 +65,9 @@ #include "norboot-target.h" #endif +#if defined(IPOD_NANO3G) +#define USE_QRCODE +#endif #define ERR_RB 0 #define ERR_OF 1 @@ -887,6 +890,40 @@ static void devel_menu(void) } #endif /* S5L87XX_DEVELOPMENT_BOOTLOADER */ +#ifdef IPOD_NANO3G +static const char qr_code_data_nano[][22] = { + /* + * qrencode -o - -t ascii -s 1 -m 0 -i 'https://rockbox.org/IPNBL' | \ + * cut -c 1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41 | \ + * awk '{ print "\"" $0 "\"," }' + */ + + "####### # #######", + "# # ## # # #", + "# ### # ## # # ### #", + "# ### # # # # # ### #", + "# ### # # ## # ### #", + "# # #### # #", + "####### # # # #######", + " ## ", + "##### ######## # # # ", + "# # #### # # #", + " # # # #", + "## # # # # ## #### ", + "# ### ## ### ", + " ## # # ####", + "####### ## # ### #", + "# # ### # ## ", + "# ### # # #### ###", + "# ### # ### ### ## ", + "# ### # #### # # ", + "# # # ## ## # #", + "####### # ## ## ", +}; +#define qr_code_data qr_code_data_nano +#endif + +#ifdef USE_QRCODE static void lcd_qr_code(const char *data, unsigned int row_stride, int x, int y, unsigned int qr_cols, unsigned int qr_rows, unsigned int modules_scale, unsigned int margin_thickness) @@ -919,41 +956,12 @@ static void lcd_qr_code(const char *data, unsigned int row_stride, lcd_update(); } -static const char qr_code_data[][22] = { - /* - * qrencode -o - -t ascii -s 1 -m 0 -i 'https://rockbox.org/IPNBL' | \ - * cut -c 1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41 | \ - * awk '{ print "\"" $0 "\"," }' - */ - - "####### # #######", - "# # ## # # #", - "# ### # ## # # ### #", - "# ### # # # # # ### #", - "# ### # # ## # ### #", - "# # #### # #", - "####### # # # #######", - " ## ", - "##### ######## # # # ", - "# # #### # # #", - " # # # #", - "## # # # # ## #### ", - "# ### ## ### ", - " ## # # ####", - "####### ## # ### #", - "# # ### # ## ", - "# ### # # #### ###", - "# ### # ### ### ## ", - "# ### # #### # # ", - "# # # ## ## # #", - "####### # ## ## ", -}; - enum { QR_COLS = sizeof(qr_code_data[0]) - 1, QR_ROWS = sizeof(qr_code_data) / sizeof(qr_code_data[0]), QR_STRIDE = sizeof(qr_code_data[0]), }; +#endif /* USE_QRCODE */ void main(void) { From 08d3332edf10e2900e66eb4460a3081af11e73db Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:24 -0400 Subject: [PATCH 222/408] opus: ARM stereo de-emphasis kernel deemphasis_stereo_simple on both cores, with the filter state kept unshifted and shifted inside the add that consumes it. Modelled: -0.53% ARMv4, -0.96% ARMv5E. Measured with the three preceding commits: e200v1 49.42 -> 48.96 MHz, Clip+ 32.83 -> 30.89 MHz. Co-Authored-By: Claude Opus 5 Change-Id: Id09297974f3b66a5a193b524d68fd8844f71cc02 --- lib/rbcodec/codecs/libopus/SOURCES | 2 + .../codecs/libopus/celt/arm/comb_filter_arm.h | 13 +++ .../libopus/celt/arm/deemph_armv4_asm.S | 101 ++++++++++++++++++ .../libopus/celt/arm/deemph_armv5e_asm.S | 97 +++++++++++++++++ .../codecs/libopus/celt/celt_decoder.c | 2 + 5 files changed, 215 insertions(+) create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/deemph_armv4_asm.S create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/deemph_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index aa726b4c22..67f46fab08 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -13,12 +13,14 @@ celt/arm/kiss_fft_armv4_asm.S celt/arm/mdct_armv4_asm.S celt/arm/comb_filter_armv4_asm.S celt/arm/denorm_armv4_asm.S +celt/arm/deemph_armv4_asm.S #elif defined(CPU_ARM) && (ARM_ARCH == 5) celt/arm/comb_filter_armv5e_asm.S celt/arm/denorm_armv5e_asm.S celt/arm/exp_rotation1_armv5e_asm.S celt/arm/haar1_armv5e_asm.S celt/arm/normres_armv5e_asm.S +celt/arm/deemph_armv5e_asm.S #endif celt/laplace.c celt/mathops.c diff --git a/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h index 7d24a1d204..a1f1baaecf 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h @@ -54,6 +54,13 @@ void comb_filter_const_armv4(opus_val32 *y, opus_val32 *x, int T, int N, void celt_sat_armv4(celt_sig *x, int n); # define celt_sat(x, n) celt_sat_armv4((x), (n)) +/* deemphasis_stereo_simple, the last stage before PCM. */ +# define OVERRIDE_DEEMPH_STEREO +void deemph_stereo_armv4(celt_sig *x0, celt_sig *x1, opus_val16 *pcm, int N, + int coef0, celt_sig *mem); +# define deemphasis_stereo_simple(in, pcm, N, coef0, mem) \ + deemph_stereo_armv4((in)[0], (in)[1], (pcm), (N), (coef0), (mem)) + # elif defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH == 5) # define OVERRIDE_COMB_FILTER_CONST @@ -67,6 +74,12 @@ void comb_filter_const_armv5e(opus_val32 *y, opus_val32 *x, int T, int N, void celt_sat_armv5e(celt_sig *x, int n); # define celt_sat(x, n) celt_sat_armv5e((x), (n)) +# define OVERRIDE_DEEMPH_STEREO +void deemph_stereo_armv5e(celt_sig *x0, celt_sig *x1, opus_val16 *pcm, int N, + int coef0, celt_sig *mem); +# define deemphasis_stereo_simple(in, pcm, N, coef0, mem) \ + deemph_stereo_armv5e((in)[0], (in)[1], (pcm), (N), (coef0), (mem)) + # endif #endif diff --git a/lib/rbcodec/codecs/libopus/celt/arm/deemph_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/deemph_armv4_asm.S new file mode 100644 index 0000000000..8336c7183c --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/deemph_armv4_asm.S @@ -0,0 +1,101 @@ +/* ARMv4 stereo de-emphasis for the CELT decoder output. + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2010 Xiph.Org Foundation + * Copyright (c) 2008 Gregory Maxwell + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/celt_decoder.c are met. + * + * Why this exists + * --------------- + * deemphasis_stereo_simple is the decoder's last stage: a one-pole filter + * per channel and the conversion to 16-bit PCM. On the ARM7TDMI the two + * smull dominate and cannot be avoided, but the compiled loop also shifts + * both filter states left once per sample and clamps to 16 bits with four + * instructions and a register holding -32768. + * + * Here the state is kept as the high word of the smull and shifted inside + * the add that consumes it, "add r8, r8, r4, lsl #1", and the clamp is the + * three-instruction asr / teq / eorne form. The incoming state from mem[] + * is a full value, so the first sample adds it unshifted. See + * deemph_armv5e_asm.S for the clamp. + * + * Bit-exact: the high word of smull(tmp, coef0<<16) shifted left one is + * exactly MULT16_32_Q15_armv4, low bit dropped included. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "deemph_armv4_asm.S must be assembled in ARM mode" +#endif + +/* These kernels are 2,892 bytes that carry 43% of the cycles an ARMv4 decode + * spends, which makes them the densest thing in the codec to put in IRAM. + * config.h sets OPUS_ARM_ICODE on the targets whose codec IRAM window has + * room; everywhere else this stays in .text. The firmware config.h it reads + * that from is assembly-safe, and libopus/config.h guards the rest of its + * includes with __ASSEMBLER__ so this include costs nothing here. */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + + .text + .align 2 + + .macro PCM16 src, tmp + add \tmp, \src, #0x800 @ PSHR32(x, 12) + mov \tmp, \tmp, asr #12 + mov lr, \tmp, asr #15 + teq lr, \tmp, asr #31 + eorne \tmp, r7, \tmp, asr #31 + strh \tmp, [r2], #2 + .endm + +/* ------------------------------------------------------------------------ + * void deemph_stereo_armv4(celt_sig *x0, celt_sig *x1, opus_val16 *pcm, + * int N, int coef0, celt_sig *mem) + * + * r0 x0, r1 x1, r2 pcm, r6 the count, r4 and r5 the two filter states, + * r7 32767, r8 and r9 the two channel sums, r10 coef0<<16, r11 and lr + * scratch, ip mem. smull needs RdLo, RdHi and Rm distinct: lr, r4/r5, r8/r9. + * ------------------------------------------------------------------------ */ + .global deemph_stereo_armv4 + .type deemph_stereo_armv4, %function +deemph_stereo_armv4: + push {r4-r11, lr} + ldr r10, [sp, #36] @ coef0 + ldr ip, [sp, #40] @ mem + mov r10, r10, lsl #16 @ into the top half of Rs, as the C does + mov r6, r3 + ldmia ip, {r4, r5} @ m0, m1, full values + mov r7, #0x7F00 + orr r7, r7, #0xFF + cmp r6, #0 + ble .Lde4_store + ldr r8, [r0], #4 + ldr r9, [r1], #4 + add r8, r8, r4 @ first sample: the state as stored + add r9, r9, r5 + b .Lde4_body +.Lde4_loop: + ldr r8, [r0], #4 + ldr r9, [r1], #4 + add r8, r8, r4, lsl #1 @ tmp0 = x0[j] + m0 + add r9, r9, r5, lsl #1 @ tmp1 = x1[j] + m1 +.Lde4_body: + smull lr, r4, r8, r10 @ m0 = r4 << 1 + smull lr, r5, r9, r10 @ m1 = r5 << 1 + PCM16 r8, r11 @ pcm[2j] + PCM16 r9, r11 @ pcm[2j+1] + subs r6, r6, #1 + bne .Lde4_loop + mov r4, r4, lsl #1 + mov r5, r5, lsl #1 +.Lde4_store: + stmia ip, {r4, r5} @ mem[0], mem[1] + pop {r4-r11, pc} + .size deemph_stereo_armv4, .-deemph_stereo_armv4 + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/arm/deemph_armv5e_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/deemph_armv5e_asm.S new file mode 100644 index 0000000000..4cc26ac96a --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/deemph_armv5e_asm.S @@ -0,0 +1,97 @@ +/* ARMv5E stereo de-emphasis for the CELT decoder output. + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2010 Xiph.Org Foundation + * Copyright (c) 2008 Gregory Maxwell + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/celt_decoder.c are met. + * + * Why this exists + * --------------- + * deemphasis_stereo_simple is the decoder's last stage: a one-pole filter + * per channel and the conversion to 16-bit PCM. The compiled loop feeds + * each channel's ldr straight into an add, a stall apiece, shifts both + * filter states left once per sample, and clamps to 16 bits with four + * instructions and a register holding -32768. + * + * Here the state is kept as the smulwb result and shifted inside the add + * that consumes it, "add r8, r8, r4, lsl #1", which removes two shifts per + * sample. The incoming state from mem[] is a full value, so the first + * sample adds it unshifted and joins the loop after the adds; nothing is + * assumed about its low bit. The clamp is the three-instruction form + * + * mov lr, y, asr #15 + * teq lr, y, asr #31 @ in range iff bits 31..15 agree + * eorne y, r7, y, asr #31 @ else 0x7FFF, or 0xFFFF8000 for negatives + * + * whose low halfword is exactly the clamped value strh stores. + * + * Bit-exact: smulwb followed by a shift is MULT16_32_Q15 on this + * architecture, the rounding shift is PSHR32(x, SIG_SHIFT), and additions + * wrap exactly as the C does. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "deemph_armv5e_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 + + .macro PCM16 src, tmp + add \tmp, \src, #0x800 @ PSHR32(x, 12) + mov \tmp, \tmp, asr #12 + mov lr, \tmp, asr #15 + teq lr, \tmp, asr #31 + eorne \tmp, r7, \tmp, asr #31 + strh \tmp, [r2], #2 + .endm + +/* ------------------------------------------------------------------------ + * void deemph_stereo_armv5e(celt_sig *x0, celt_sig *x1, opus_val16 *pcm, + * int N, int coef0, celt_sig *mem) + * + * r0 x0, r1 x1, r2 pcm, r3 and r6 the count, r4 and r5 the two filter + * states, r7 32767, r8 and r9 the two channel sums, r10 coef0, r11 and lr + * scratch, ip mem. + * ------------------------------------------------------------------------ */ + .global deemph_stereo_armv5e + .type deemph_stereo_armv5e, %function +deemph_stereo_armv5e: + push {r4-r11, lr} + ldr r10, [sp, #36] @ coef0; smulwb reads its low halfword + ldr ip, [sp, #40] @ mem + mov r6, r3 + ldmia ip, {r4, r5} @ m0, m1, full values + mov r7, #0x7F00 + orr r7, r7, #0xFF + cmp r6, #0 + ble .Lde5_store + ldr r8, [r0], #4 + ldr r9, [r1], #4 + add r8, r8, r4 @ first sample: the state as stored + add r9, r9, r5 + b .Lde5_body +.Lde5_loop: + ldr r8, [r0], #4 + ldr r9, [r1], #4 + add r8, r8, r4, lsl #1 @ tmp0 = x0[j] + m0 + add r9, r9, r5, lsl #1 @ tmp1 = x1[j] + m1 +.Lde5_body: + smulwb r4, r8, r10 @ m0 = this << 1 + smulwb r5, r9, r10 @ m1 = this << 1 + PCM16 r8, r11 @ pcm[2j] + PCM16 r9, r11 @ pcm[2j+1] + subs r6, r6, #1 + bne .Lde5_loop + mov r4, r4, lsl #1 + mov r5, r5, lsl #1 +.Lde5_store: + stmia ip, {r4, r5} @ mem[0], mem[1] + pop {r4-r11, pc} + .size deemph_stereo_armv5e, .-deemph_stereo_armv5e + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/celt_decoder.c b/lib/rbcodec/codecs/libopus/celt/celt_decoder.c index 6aef870c07..8d5e8f63a5 100644 --- a/lib/rbcodec/codecs/libopus/celt/celt_decoder.c +++ b/lib/rbcodec/codecs/libopus/celt/celt_decoder.c @@ -227,6 +227,7 @@ void opus_custom_decoder_destroy(CELTDecoder *st) /* Special case for stereo with no downsampling and no accumulation. This is quite common and we can make it faster by processing both channels in the same loop, reducing overhead due to the dependency loop in the IIR filter. */ +#ifndef OVERRIDE_DEEMPH_STEREO static void deemphasis_stereo_simple(celt_sig *in[], opus_val16 *pcm, int N, const opus_val16 coef0, celt_sig *mem) { @@ -253,6 +254,7 @@ static void deemphasis_stereo_simple(celt_sig *in[], opus_val16 *pcm, int N, con mem[1] = m1; } #endif +#endif #ifndef RESYNTH static From 0c4345475a9750c66b00f85e28147f2e620447c4 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:25 -0400 Subject: [PATCH 223/408] opus: ARMv5E assembly for the backward MDCT inner loops Pre-rotation, post-rotation and mirror, with a packed-twiddle complex multiply throughout. Modelled: -8.0% ARMv5E against the C loops. Co-Authored-By: Claude Opus 5 Change-Id: I3fcaf2343e04e3f3f4462fc442f8fac623729631 --- lib/rbcodec/codecs/libopus/SOURCES | 1 + .../codecs/libopus/celt/arm/mdct_armv4.h | 6 + .../codecs/libopus/celt/arm/mdct_armv5e.h | 69 ++++ .../codecs/libopus/celt/arm/mdct_armv5e_asm.S | 304 ++++++++++++++++++ lib/rbcodec/codecs/libopus/celt/mdct.c | 9 +- 5 files changed, 385 insertions(+), 4 deletions(-) create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index 67f46fab08..996465a7a5 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -15,6 +15,7 @@ celt/arm/comb_filter_armv4_asm.S celt/arm/denorm_armv4_asm.S celt/arm/deemph_armv4_asm.S #elif defined(CPU_ARM) && (ARM_ARCH == 5) +celt/arm/mdct_armv5e_asm.S celt/arm/comb_filter_armv5e_asm.S celt/arm/denorm_armv5e_asm.S celt/arm/exp_rotation1_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h index 3b39785685..6c4191fa7f 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h @@ -41,6 +41,11 @@ #define OVERRIDE_MDCT_POSTROT #define OVERRIDE_MDCT_MIRROR +#define mdct_prerot_opt mdct_prerot_armv4 +#define mdct_postrot_opt mdct_postrot_armv4 + +#define mdct_mirror_opt mdct_mirror_armv4 + /* step is a byte stride, so the caller scales by sizeof(kiss_fft_scalar). */ void mdct_prerot_armv4(const kiss_fft_scalar *xp1, const kiss_fft_scalar *xp2, @@ -51,6 +56,7 @@ void mdct_prerot_armv4(const kiss_fft_scalar *xp1, void mdct_postrot_armv4(kiss_fft_scalar *yp0, kiss_fft_scalar *yp1, const kiss_twiddle_scalar *t, int N4, int count); + void mdct_mirror_armv4(kiss_fft_scalar *xp1, kiss_fft_scalar *yp1, const opus_val16 *wp1, const opus_val16 *wp2, int count); diff --git a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h new file mode 100644 index 0000000000..32d68ba1fa --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h @@ -0,0 +1,69 @@ +/* Copyright (c) 2025 Xiph.Org Foundation and contributors + Copyright (c) 2026 Michael Giacomelli + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef MDCT_ARMv5E_H +#define MDCT_ARMv5E_H + +/* Hand-written inner loops for the backward MDCT, the ARMv5E counterpart of + arm/mdct_armv4.h. The compiled loops on this architecture already use the + 32x16 multiply; what they miss is the packed halfword operand, the + multiply-accumulate and the multi-register transfer. See + celt/arm/mdct_armv5e_asm.S. + + These are bit-exact with the C on this architecture, unlike the ARMv4 + kernels, which are more accurate than theirs. + + Building with OPUS_ARM_NO_MDCT_ASM selects the C loops instead, which is + how the two are compared. */ + +#if defined(OPUS_ARM_INLINE_EDSP) && defined(FIXED_POINT) \ + && (ARM_ARCH >= 5) && !defined(OPUS_ARM_NO_MDCT_ASM) + +#define OVERRIDE_MDCT_PREROT +#define OVERRIDE_MDCT_POSTROT +#define OVERRIDE_MDCT_MIRROR + +#define mdct_prerot_opt mdct_prerot_armv5e +#define mdct_postrot_opt mdct_postrot_armv5e + +#define mdct_mirror_opt mdct_mirror_armv5e + +/* step is a byte stride, so the caller scales by sizeof(kiss_fft_scalar). */ +void mdct_prerot_armv5e(const kiss_fft_scalar *xp1, + const kiss_fft_scalar *xp2, + const kiss_twiddle_scalar *t, + const opus_int16 *bitrev, + kiss_fft_scalar *yp, int N4, int step); + +void mdct_postrot_armv5e(kiss_fft_scalar *yp0, kiss_fft_scalar *yp1, + const kiss_twiddle_scalar *t, int N4, int count); + +void mdct_mirror_armv5e(kiss_fft_scalar *xp1, kiss_fft_scalar *yp1, + const opus_val16 *wp1, const opus_val16 *wp2, + int count); + +#endif + +#endif /* MDCT_ARMv5E_H */ diff --git a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e_asm.S new file mode 100644 index 0000000000..5a458813ab --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e_asm.S @@ -0,0 +1,304 @@ +/* ARMv5E inner loops for the CELT backward MDCT. + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2008 Xiph.Org Foundation + * Copyright (c) 2008 Gregory Maxwell + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/mdct.c are met. + * + * Why this exists + * --------------- + * On ARMv5E the compiled rotations already get the 32x16 multiply, because + * MULT16_32_Q15 resolves to smulwb in arm/fixed_armv5e.h. What they never + * get is the rest of the instruction set: mdct.o from a Sansa Clip+ build + * contains 34 smulwb and not one smulwt, smlaw, ldrd or multi-register + * transfer. Three things follow. + * + * The twiddles are loaded one halfword at a time. A kiss_twiddle_scalar is + * 16 bits, so t[i] and t[i+1] are one aligned word; unrolling by two lets a + * single ldr fetch both and smulwb/smulwt pick between them. That halves + * the twiddle loads, and an ldr also resolves a cycle sooner than an ldrsh. + * + * Each product pair is narrowed twice. S_MUL(a,t0)+S_MUL(b,t1) compiles to + * two smulwb, two shifts and an add, but smulwb followed by smlawb leaves + * the sum at Q16 and needs one shift. This is bit-exact with the C: both + * products are still truncated separately inside their own instruction, and + * the shift distributes over the sum. + * + * The subtraction is deliberately not fused. smlaw only adds, so fusing it + * would mean negating a twiddle, and truncation is not symmetric about + * zero: trunc(a)+trunc(-b) carries a whole unit of bias where + * trunc(a)-trunc(b) carries none. It would also cost an rsb per twiddle, + * which the packed form cannot supply. Two smulw and a shifted subtract + * stay. + * + * Accuracy is therefore identical to the C path on this architecture, not + * better as on ARMv4. The exact smull/smlal narrowing the ARMv4 kernels + * use is worth about 9.5 dB here too, but it costs a register pair and + * three issue cycles against one, which is a different trade on this core. + * + * Word loads need a word-aligned table and an even N4. The static CELT + * modes give both, but neither is guaranteed by the API, so each kernel + * tests its pointers and falls through to a scalar loop that doubles as the + * odd-count tail. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "mdct_armv5e_asm.S must be assembled in ARM mode" +#endif + +/* config.h decides whether the prime factor transform is built, and the + post-rotation kernel below only exists when it is. libopus/config.h + guards its own includes with __ASSEMBLER__, so this costs nothing here. */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + + .text + .align 2 + +/* The scalar paths below are the odd-count tail and the fallback for a table + the unroll cannot address as words. Neither runs on the static CELT modes, + so build with -DMDCT_ARMV5E_FORCE_SCALAR to route everything through them + and confirm they still agree with the C. */ + .macro FORCE_SCALAR +#ifdef MDCT_ARMV5E_FORCE_SCALAR + orr ip, ip, #1 +#endif + .endm + + +/* ------------------------------------------------------------------------ + * void mdct_prerot_armv5e(const kiss_fft_scalar *xp1, + * const kiss_fft_scalar *xp2, + * const kiss_twiddle_scalar *t, + * const opus_int16 *bitrev, + * kiss_fft_scalar *yp, int N4, int step) + * + * yr = xp2*t[i] + xp1*t[N4+i]; yi = xp1*t[i] - xp2*t[N4+i] + * yp[2*rev+1] = yr; yp[2*rev] = yi; xp1 += step; xp2 -= step + * + * step is in bytes. The output pair is 8 contiguous bytes even though the + * bitrev table scatters where it lands, so it stores as one stm. + * + * r10 holds t[i] packed with t[i+1] and r11 holds t[N4+i] with t[N4+i+1]; + * \sel picks the half. The scalar path loads with ldrsh and passes b, + * which reads the same bits. + * ------------------------------------------------------------------------ */ + .macro PREROT_ONE sel + ldr r5, [r1], -r6 @ x2 = *xp2, xp2 -= step + ldr r9, [r0], r6 @ x1 = *xp1, xp1 += step, and x2's + @ interlock is covered by it + smulw\sel lr, r5, r10 @ x2 * t[i] + smlaw\sel lr, r9, r11, lr @ + x1 * t[N4+i] -> yr at Q16 + smulw\sel ip, r9, r10 @ x1 * t[i] + smulw\sel r9, r5, r11 @ x2 * t[N4+i] + ldrsh r5, [r3], #2 @ rev = *bitrev++ + sub ip, ip, r9 + mov lr, lr, lsl #1 @ yr + mov ip, ip, lsl #1 @ yi + add r5, r4, r5, lsl #3 + stm r5, {ip, lr} @ yp[2*rev] = yi, yp[2*rev+1] = yr + .endm + + .global mdct_prerot_armv5e + .type mdct_prerot_armv5e, %function +mdct_prerot_armv5e: + push {r4-r11, lr} + sub sp, sp, #4 + ldr r4, [sp, #40] @ yp + ldr r5, [sp, #44] @ N4 + ldr r6, [sp, #48] @ step, in bytes + mov r7, r5, lsl #1 @ byte offset from t[i] to t[N4+i] + and r9, r5, #1 @ odd sample left over by the unroll + mov r8, r5, lsr #1 @ pairs + orr ip, r2, r7 @ word-addressable table and stride? + FORCE_SCALAR + tst ip, #3 + movne r8, #0 @ no: run the whole thing scalar + movne r9, r5 + str r9, [sp] @ scalar count, reloaded once + cmp r8, #0 + beq .Lpre5_scalar +.Lpre5_pairs: + ldr r11, [r2, r7] @ t[N4+i] : t[N4+i+1] + ldr r10, [r2], #4 @ t[i] : t[i+1] + PREROT_ONE b + PREROT_ONE t + subs r8, r8, #1 + bne .Lpre5_pairs +.Lpre5_scalar: + ldr r8, [sp] + cmp r8, #0 + beq .Lpre5_done +.Lpre5_sloop: + ldrsh r11, [r2, r7] @ t[N4+i] + ldrsh r10, [r2], #2 @ t[i] + PREROT_ONE b + subs r8, r8, #1 + bne .Lpre5_sloop +.Lpre5_done: + add sp, sp, #4 + pop {r4-r11, pc} + .size mdct_prerot_armv5e, .-mdct_prerot_armv5e + + +/* ------------------------------------------------------------------------ + * void mdct_postrot_armv5e(kiss_fft_scalar *yp0, kiss_fft_scalar *yp1, + * const kiss_twiddle_scalar *t, int N4, int count) + * + * Walks the buffer from both ends at once, so each end reads a contiguous + * complex pair with one ldm. Four twiddle streams run at once: t[i] and + * t[N4+i] climb, t[N4-i-1] and t[N2-i-1] descend. Both directions pack, + * the descending ones in reverse, so the unrolled pair reads them t first + * and b second. + * + * Every read happens before every write, which is what makes the doubled + * middle pair produced by an odd N4 come out the same as the C. + * ------------------------------------------------------------------------ */ + .macro POSTROT_ONE sel, dsel + ldm r0, {r8, r9} @ im = yp0[0], re = yp0[1] + smulw\sel lr, r9, r10 @ re * t0 + smlaw\sel lr, r8, r11, lr @ + im * t1 -> yr at Q16 + smulw\sel ip, r9, r11 @ re * t1 + smulw\sel r8, r8, r10 @ im * t0 + mov lr, lr, lsl #1 @ yr + sub ip, ip, r8 + mov ip, ip, lsl #1 @ yi + ldm r1, {r8, r9} @ im = yp1[0], re = yp1[1] + str lr, [r0] @ yp0[0] = yr + smulw\dsel lr, r9, r5 @ re * t0b + smlaw\dsel lr, r8, r6, lr @ + im * t1b -> yr2 at Q16 + smulw\dsel r9, r9, r6 @ re * t1b + smulw\dsel r8, r8, r5 @ im * t0b + mov lr, lr, lsl #1 @ yr2 + sub r9, r9, r8 + str lr, [r1] @ yp1[0] = yr2 + str ip, [r1, #4] @ yp1[1] = yi + mov r9, r9, lsl #1 @ yi2 + str r9, [r0, #4] @ yp0[1] = yi2 + add r0, r0, #8 + sub r1, r1, #8 + .endm + + .align 2 + .global mdct_postrot_armv5e + .type mdct_postrot_armv5e, %function +mdct_postrot_armv5e: + push {r4-r11, lr} + sub sp, sp, #4 + ldr r4, [sp, #40] @ count; r4 counts, r8 and r9 are scratch + mov r7, r3, lsl #1 @ byte offset from t[k] to t[N4+k] + sub r8, r3, #1 + add r3, r2, r8, lsl #1 @ descending pointer, at t[N4-i-1] + and r9, r4, #1 @ odd iteration left over + eor ip, r3, #2 @ its word sits two bytes below + orr ip, ip, r2 + orr ip, ip, r7 + FORCE_SCALAR + tst ip, #3 + movne r9, r4 @ not word-addressable: go scalar + mov r4, r4, lsr #1 @ pairs + movne r4, #0 + str r9, [sp] + cmp r4, #0 + beq .Lpost5_scalar +.Lpost5_pairs: + ldr r10, [r2] @ t[i] : t[i+1] + ldr r11, [r2, r7] @ t[N4+i] : t[N4+i+1] + add lr, r3, r7 + ldr r5, [r3, #-2] @ t[N4-i-2] : t[N4-i-1] + ldr r6, [lr, #-2] @ t[N2-i-2] : t[N2-i-1] + add r2, r2, #4 + sub r3, r3, #4 + POSTROT_ONE b, t + POSTROT_ONE t, b + subs r4, r4, #1 + bne .Lpost5_pairs +.Lpost5_scalar: + ldr r4, [sp] + cmp r4, #0 + beq .Lpost5_done +.Lpost5_sloop: + ldrsh r11, [r2, r7] @ t[N4+i] + ldrsh r10, [r2], #2 @ t[i] + ldrsh r6, [r3, r7] @ t[N2-i-1] + ldrsh r5, [r3], #-2 @ t[N4-i-1] + POSTROT_ONE b, b + subs r4, r4, #1 + bne .Lpost5_sloop +.Lpost5_done: + add sp, sp, #4 + pop {r4-r11, pc} + .size mdct_postrot_armv5e, .-mdct_postrot_armv5e + + +/* ------------------------------------------------------------------------ + * void mdct_mirror_armv5e(kiss_fft_scalar *xp1, kiss_fft_scalar *yp1, + * const opus_val16 *wp1, const opus_val16 *wp2, + * int count) + * + * The TDAC fold. The two data pointers walk in opposite directions and + * never meet, so nothing there bursts, but the two window pointers pack + * exactly as the twiddles do. + * ------------------------------------------------------------------------ */ + .macro MIRROR_ONE sel, dsel + ldr r9, [r0] @ x1 = *xp1 + ldr ip, [r1] @ x2 = *yp1 + smulw\dsel lr, ip, r6 @ x2 * w2 + smulw\sel r8, r9, r5 @ x1 * w1 + sub lr, lr, r8 + mov lr, lr, lsl #1 + str lr, [r1], #4 @ *yp1++ = w2*x2 - w1*x1 + smulw\sel lr, ip, r5 @ x2 * w1 + smlaw\dsel lr, r9, r6, lr @ + x1 * w2 + mov lr, lr, lsl #1 + str lr, [r0], #-4 @ *xp1-- = w1*x2 + w2*x1 + .endm + + .align 2 + .global mdct_mirror_armv5e + .type mdct_mirror_armv5e, %function +mdct_mirror_armv5e: + push {r4-r11, lr} + sub sp, sp, #4 + ldr r4, [sp, #40] @ count; r4 counts, r8 and r9 are scratch + and r9, r4, #1 + eor ip, r3, #2 + orr ip, ip, r2 + FORCE_SCALAR + tst ip, #3 + movne r9, r4 + mov r4, r4, lsr #1 + movne r4, #0 + str r9, [sp] + cmp r4, #0 + beq .Lmir5_scalar +.Lmir5_pairs: + ldr r5, [r2], #4 @ w1[i] : w1[i+1] + ldr r6, [r3, #-2] @ w2[-i-1] : w2[-i] + sub r3, r3, #4 + MIRROR_ONE b, t + MIRROR_ONE t, b + subs r4, r4, #1 + bne .Lmir5_pairs +.Lmir5_scalar: + ldr r4, [sp] + cmp r4, #0 + beq .Lmir5_done +.Lmir5_sloop: + ldrsh r5, [r2], #2 @ w1 + ldrsh r6, [r3], #-2 @ w2 + MIRROR_ONE b, b + subs r4, r4, #1 + bne .Lmir5_sloop +.Lmir5_done: + add sp, sp, #4 + pop {r4-r11, pc} + .size mdct_mirror_armv5e, .-mdct_mirror_armv5e + + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/mdct.c b/lib/rbcodec/codecs/libopus/celt/mdct.c index 1b13863d59..41ce630492 100644 --- a/lib/rbcodec/codecs/libopus/celt/mdct.c +++ b/lib/rbcodec/codecs/libopus/celt/mdct.c @@ -53,6 +53,7 @@ #include "mathops.h" #if defined(OPUS_ARM_ASM) #include "arm/mdct_armv4.h" +#include "arm/mdct_armv5e.h" #endif #include "stack_alloc.h" @@ -269,8 +270,8 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca const kiss_twiddle_scalar * OPUS_RESTRICT t = &trig[0]; const opus_int16 * OPUS_RESTRICT bitrev = l->kfft[shift]->bitrev; #ifdef OVERRIDE_MDCT_PREROT - mdct_prerot_armv4(xp1, xp2, t, bitrev, yp, N4, - 2*stride*(int)sizeof(kiss_fft_scalar)); + mdct_prerot_opt(xp1, xp2, t, bitrev, yp, N4, + 2*stride*(int)sizeof(kiss_fft_scalar)); #else for(i=0;i>1 to handle odd N4. When N4 is odd, the middle pair will be computed twice. */ #ifdef OVERRIDE_MDCT_POSTROT - mdct_postrot_armv4(yp0, yp1, t, N4, (N4+1)>>1); + mdct_postrot_opt(yp0, yp1, t, N4, (N4+1)>>1); #else for(i=0;i<(N4+1)>>1;i++) { @@ -341,7 +342,7 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca const opus_val16 * OPUS_RESTRICT wp2 = window+overlap-1; #ifdef OVERRIDE_MDCT_MIRROR - mdct_mirror_armv4(xp1, yp1, wp1, wp2, overlap/2); + mdct_mirror_opt(xp1, yp1, wp1, wp2, overlap/2); #else for(i = 0; i < overlap/2; i++) { From ae223933bf1b78b51633b0cc8b6545160c2868e2 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:26 -0400 Subject: [PATCH 224/408] opus: ARMv5E FFT butterflies radix-3, 4 and 5. The gain is bookkeeping: twiddles addressed by displacement from one base register, post-indexed stores, and C_MUL's Q15 doubling folded into the add that consumes it. Modelled: -4.72% ARMv5E, opus_fft_impl -25.3%. Measured: Clip+ 30.89 -> 29.33 MHz. Co-Authored-By: Claude Opus 5 Change-Id: I173667ebb13308f83bd6fcb4e876f5a56c698cd3 --- lib/rbcodec/codecs/libopus/SOURCES | 1 + .../codecs/libopus/celt/arm/kiss_fft_armv5e.h | 27 + .../libopus/celt/arm/kiss_fft_armv5e_asm.S | 595 ++++++++++++++++++ 3 files changed, 623 insertions(+) create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index 996465a7a5..279b0f6a6a 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -15,6 +15,7 @@ celt/arm/comb_filter_armv4_asm.S celt/arm/denorm_armv4_asm.S celt/arm/deemph_armv4_asm.S #elif defined(CPU_ARM) && (ARM_ARCH == 5) +celt/arm/kiss_fft_armv5e_asm.S celt/arm/mdct_armv5e_asm.S celt/arm/comb_filter_armv5e_asm.S celt/arm/denorm_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv5e.h b/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv5e.h index 9eca183d77..3fc1e2d823 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv5e.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv5e.h @@ -113,6 +113,33 @@ } \ while(0) +/* Assembly radix-3, radix-4 and radix-5 passes, in kiss_fft_armv5e_asm.S. + The C_MUL above is already as tight as the core allows; what the assembly + recovers is the bookkeeping gcc cannot keep resident. Measured over eight + packets of stereo decode, 16.0% of the instructions executed in the + compiled radix-4 pass are stack traffic, 11.4% of radix-3 and 23.0% of + radix-5, almost all of it output pointers spilled and reloaded around the + complex multiplies. Radix-2 is left in C: it needs no twiddle pointers + and spills only 4.7%, so there is nothing to win. The kernels take + st->twiddles rather than st, so the assembly needs no knowledge of the + layout of kiss_fft_state. */ +#ifndef OPUS_ARM_NO_FFT_ASM +#define OVERRIDE_kf_bfly3 +#define OVERRIDE_kf_bfly4 +#define OVERRIDE_kf_bfly5 + +void kf_bfly3_armv5e(kiss_fft_cpx *Fout, const kiss_twiddle_cpx *tw, + int fstride, int m, int N, int mm); +void kf_bfly4_armv5e(kiss_fft_cpx *Fout, const kiss_twiddle_cpx *tw, + int fstride, int m, int N, int mm); +void kf_bfly5_armv5e(kiss_fft_cpx *Fout, const kiss_twiddle_cpx *tw, + int fstride, int m, int N, int mm); + +#define kf_bfly3(Fout, fstride, st, m, N, mm) kf_bfly3_armv5e((Fout), (st)->twiddles, (int)(fstride), (m), (N), (mm)) +#define kf_bfly4(Fout, fstride, st, m, N, mm) kf_bfly4_armv5e((Fout), (st)->twiddles, (int)(fstride), (m), (N), (mm)) +#define kf_bfly5(Fout, fstride, st, m, N, mm) kf_bfly5_armv5e((Fout), (st)->twiddles, (int)(fstride), (m), (N), (mm)) +#endif /* OPUS_ARM_NO_FFT_ASM */ + #endif /* FIXED_POINT */ #endif /* KISS_FFT_GUTS_H */ diff --git a/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv5e_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv5e_asm.S new file mode 100644 index 0000000000..ec4e8f819c --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv5e_asm.S @@ -0,0 +1,595 @@ +/* ARMv5E FFT butterflies for the CELT fixed-point kiss_fft. + * + * Copyright (c) 2003-2004, Mark Borgerding + * Copyright (c) 2005-2007, Xiph.Org Foundation + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/kiss_fft.c are met. + * + * Why this exists + * --------------- + * The argument here is not the one behind kiss_fft_armv4_asm.S. On + * ARM926EJ-S a load costs one cycle and a load multiple of n costs n, so + * bursting buys nothing; upstream's arm/kiss_fft_armv5e.h already supplies a + * packed-twiddle complex multiply that is four DSP multiplies and a + * subtraction, which is optimal. What is left is bookkeeping. + * + * Measured over eight packets of stereo decode, 16.0% of the instructions + * executed in the compiled radix-4 pass are stack traffic: gcc cannot hold + * four output pointers, a twiddle base and a stride at once while a complex + * multiply is in flight, so it spills the pointers and reloads them every + * pass. The radix-3 pass loses 11.4% the same way and the radix-5 pass + * 23.0%. + * + * These routines keep the bookkeeping resident and park only what genuinely + * will not fit. Three things pay for that: + * + * - The twiddle addresses are a base and a byte displacement rather than + * three running pointers, so one register holds what three did and the + * stride never has to be reloaded to advance them. Pass j reads + * twiddles[j*fstride] as [base, disp], twiddles[2*j*fstride] as + * [base, disp, lsl #1], and only the radix-4 third twiddle needs an + * explicit 3*disp. + * + * - Every store is post-indexed, so the four output pointers advance for + * free and the loop needs no pointer arithmetic at all. + * + * - The Q15 doubling that C_MUL applies to each product is deferred into + * the add or subtract that consumes it, as an immediate shift, which is + * free on this core. Deferring is exact: SHL32 is a wrapping shift, so + * 2*(a+b) and 2*a+2*b agree in all 32 bits. + * + * Every pair load is ordered so the register the next instruction needs + * landed at least two instructions earlier, which is what the five-stage + * pipeline wants; the loop counter's decrement fills the one remaining slot. + * + * Bit-exactness. The multiply sequence is the one in + * arm/kiss_fft_armv5e.h -- smulwb, smulwb, smulwt, smlawt, then a + * subtraction -- so every product truncates exactly where the C did, and the + * deferred doubling is exact as above. Output is bit-identical to the C + * path, which the test harness checks sample by sample. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "kiss_fft_armv5e_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 + +/* C_MUL, packed twiddle. + * + * \pr, \pi the point, .r and .i, consumed + * \tw the packed twiddle: tw.r in bits 15:0, tw.i in 31:16 + * \t scratch, consumed + * result \pr = m.r/2, \pi = m.i/2 (the caller doubles as it consumes) + * + * Matches C_MUL in arm/kiss_fft_armv5e.h term for term: + * m.r = ((p.r*tw.r)>>16 - (p.i*tw.i)>>16) << 1 + * m.i = ((p.i*tw.r)>>16 + (p.r*tw.i)>>16) << 1 + */ + .macro CMUL_HALF pr, pi, tw, t + smulwt \t, \pi, \tw @ p.i*tw.i >> 16 + smulwb \pi, \pi, \tw @ p.i*tw.r >> 16 + smlawt \pi, \pr, \tw, \pi @ + p.r*tw.i >> 16 -> m.i/2 + smulwb \pr, \pr, \tw @ p.r*tw.r >> 16 + sub \pr, \pr, \t @ - p.i*tw.i >> 16 -> m.r/2 + .endm + + +/* ------------------------------------------------------------------------ + * void kf_bfly4_armv5e(kiss_fft_cpx *Fout, const kiss_twiddle_cpx *tw, + * int fstride, int m, int N, int mm) + * + * r0 = Fout r1 = tw r2 = fstride r3 = m [sp+0] = N [sp+4] = mm + * + * Inner-loop registers: + * r0-r3 the four output pointers, Fout + 0, m, 2m, 3m, post-incremented + * r4 twiddle base r5 twiddle displacement in bytes + * r6 pass counter r7-r11, ip, lr scratch + * + * Frame, 40 bytes. The first sixteen are the placed spill: the inner loop + * parks one complex value there, the peeled pass two. + * 0 v.r / T0.r 4 v.i / T1.r 8 T1.i 12 T0.i + * 16 fstride, bytes 20 Fout for the next i 24 N left + * 28 mm*8 32 M = m*8 36 m + * ------------------------------------------------------------------------ */ + .global kf_bfly4_armv5e + .type kf_bfly4_armv5e, %function +kf_bfly4_armv5e: + push {r4-r11, lr} + cmp r3, #1 + bne .Lb4_general + +/* Degenerate case: every twiddle is 1, so four consecutive complex points -- + eight consecutive words -- go in and eight come out with no multiplies. + This is the one radix-4 body that fits entirely in registers. */ + ldr r1, [sp, #36] @ N +.Lb4_m1: + ldm r0, {r4, r5, r6, r7, r8, r9, r10, r11} + sub ip, r4, r8 @ s0.r = F0.r - F2.r + sub lr, r5, r9 @ s0.i + add r4, r4, r8 @ F0.r += F2.r + add r5, r5, r9 + add r8, r6, r10 @ s1.r = F1.r + F3.r + add r9, r7, r11 @ s1.i + sub r6, r6, r10 @ d.r = F1.r - F3.r + sub r7, r7, r11 @ d.i + sub r10, r4, r8 @ F2.r = F0.r - s1.r + sub r11, r5, r9 @ F2.i + add r4, r4, r8 @ F0.r += s1.r + add r5, r5, r9 + add r8, ip, r7 @ F1.r = s0.r + d.i + sub r9, lr, r6 @ F1.i = s0.i - d.r + sub ip, ip, r7 @ F3.r = s0.r - d.i + add lr, lr, r6 @ F3.i = s0.i + d.r + stmia r0!, {r4, r5, r8, r9, r10, r11, ip, lr} + subs r1, r1, #1 + bne .Lb4_m1 + pop {r4-r11, pc} + +.Lb4_general: + sub sp, sp, #40 + ldr r7, [sp, #76] @ N + ldr ip, [sp, #80] @ mm + mov r2, r2, lsl #2 @ fstride in bytes, 4 per kiss_twiddle_cpx + mov ip, ip, lsl #3 @ mm * sizeof(kiss_fft_cpx) + mov lr, r3, lsl #3 @ M = m * sizeof(kiss_fft_cpx) + str r2, [sp, #16] + str r0, [sp, #20] + str r7, [sp, #24] + str ip, [sp, #28] + str lr, [sp, #32] + str r3, [sp, #36] + mov r4, r1 @ twiddle base, invariant + +.Lb4_outer: + ldr r0, [sp, #20] @ Fout for this i + ldr r7, [sp, #28] @ mm*8 + ldr ip, [sp, #32] @ M + add r7, r0, r7 + str r7, [sp, #20] @ Fout for i+1 + add r1, r0, ip @ p1 = Fout + m + add r2, r1, ip @ p2 = Fout + 2m + add r3, r2, ip @ p3 = Fout + 3m + ldr r6, [sp, #36] @ pass counter = m + mov r5, #0 @ twiddle displacement + +/* Pass 0 twiddles by twiddles[0], which is 1, so it is peeled: no twiddle + load and no multiply. In fixed point twiddles[0] is 32767 rather than + 32768, so skipping it also drops a small systematic gain error. These + values are full scale, not the halved form the inner loop carries, so the + combine here has no doubling in it. */ + ldr r8, [r0] @ A.r + ldr r9, [r0, #4] @ A.i + ldr r10, [r2] @ C.r + ldr r11, [r2, #4] @ C.i + add r7, r8, r10 @ T0.r = A.r + C.r + sub r8, r8, r10 @ T1.r = A.r - C.r + add r10, r9, r11 @ T0.i + sub r9, r9, r11 @ T1.i + str r7, [sp, #0] + str r8, [sp, #4] + str r9, [sp, #8] + str r10, [sp, #12] + ldr r8, [r1] @ B.r + ldr r9, [r1, #4] @ B.i + ldr r10, [r3] @ D.r + ldr r11, [r3, #4] @ D.i + add r7, r8, r10 @ u.r = B.r + D.r + sub r8, r8, r10 @ v.r = B.r - D.r + add r10, r9, r11 @ u.i + sub r9, r9, r11 @ v.i + ldr r11, [sp, #0] @ T0.r + ldr ip, [sp, #12] @ T0.i + add r11, r11, r7 @ out0.r = T0.r + u.r + sub r7, r11, r7, lsl #1 @ out2.r = T0.r - u.r + add ip, ip, r10 @ out0.i + sub r10, ip, r10, lsl #1 @ out2.i + str r11, [r0], #4 + str ip, [r0], #4 + str r7, [r2], #4 + str r10, [r2], #4 + ldr r11, [sp, #4] @ T1.r + ldr ip, [sp, #8] @ T1.i + add r7, r11, r9 @ out1.r = T1.r + v.i + sub r10, ip, r8 @ out1.i = T1.i - v.r + str r7, [r1], #4 + str r10, [r1], #4 + sub r7, r11, r9 @ out3.r = T1.r - v.i + add r10, ip, r8 @ out3.i = T1.i + v.r + str r7, [r3], #4 + str r10, [r3], #4 + subs r6, r6, #1 + beq .Lb4_next_i + +/* Passes 1 .. m-1. D and B are twiddled first and folded straight into + u = (B+D)/2 and v = (B-D)/2, which frees the multiply's five registers + again before C needs them; only v has to be parked. The counter's + decrement sits at the top to cover the one load-use slot that had nothing + else to fill it, and nothing between there and the branch touches the + flags. */ +.Lb4_inner: + ldr r7, [sp, #16] @ fstride, bytes + subs r6, r6, #1 + add r5, r5, r7 @ next twiddle displacement + add r7, r5, r5, lsl #1 @ 3 * displacement + ldr r7, [r4, r7] @ tw3, packed + ldr r9, [r3, #4] @ D.i + ldr r8, [r3] @ D.r + CMUL_HALF r8, r9, r7, r10 @ D.r/2 -> r8, D.i/2 -> r9 + ldr r7, [r4, r5] @ tw1, packed + ldr r11, [r1, #4] @ B.i + ldr r10, [r1] @ B.r + CMUL_HALF r10, r11, r7, ip @ B.r/2 -> r10, B.i/2 -> r11 + add r7, r10, r8 @ u.r = (B.r + D.r)/2 + sub r8, r10, r8 @ v.r + add r10, r11, r9 @ u.i + sub r9, r11, r9 @ v.i + str r8, [sp, #0] @ park v.r + str r9, [sp, #4] @ park v.i + ldr r8, [r4, r5, lsl #1] @ tw2, packed + ldr r11, [r2, #4] @ C.i + ldr r9, [r2] @ C.r + CMUL_HALF r9, r11, r8, ip @ C.r/2 -> r9, C.i/2 -> r11 + ldr r8, [r0] @ A.r + ldr ip, [r0, #4] @ A.i + add lr, r8, r9, lsl #1 @ T0.r = A.r + C.r + sub r8, r8, r9, lsl #1 @ T1.r + add r9, ip, r11, lsl #1 @ T0.i + sub ip, ip, r11, lsl #1 @ T1.i + add lr, lr, r7, lsl #1 @ out0.r = T0.r + u.r + sub r7, lr, r7, lsl #2 @ out2.r = T0.r - u.r + add r9, r9, r10, lsl #1 @ out0.i + sub r10, r9, r10, lsl #2 @ out2.i + str lr, [r0], #4 + str r9, [r0], #4 + str r7, [r2], #4 + str r10, [r2], #4 + ldr r11, [sp, #4] @ v.i + ldr lr, [sp, #0] @ v.r + add r7, r8, r11, lsl #1 @ out1.r = T1.r + v.i + sub r9, ip, lr, lsl #1 @ out1.i = T1.i - v.r + str r7, [r1], #4 + str r9, [r1], #4 + sub r7, r8, r11, lsl #1 @ out3.r = T1.r - v.i + add r9, ip, lr, lsl #1 @ out3.i = T1.i + v.r + str r7, [r3], #4 + str r9, [r3], #4 + bne .Lb4_inner + +.Lb4_next_i: + ldr r7, [sp, #24] @ N left + subs r7, r7, #1 + str r7, [sp, #24] + bne .Lb4_outer + add sp, sp, #40 + pop {r4-r11, pc} + .size kf_bfly4_armv5e, .-kf_bfly4_armv5e + + +/* ------------------------------------------------------------------------ + * void kf_bfly3_armv5e(kiss_fft_cpx *Fout, const kiss_twiddle_cpx *tw, + * int fstride, int m, int N, int mm) + * + * Inner-loop registers: + * r0-r2 the three output pointers, Fout + 0, m, 2m, post-incremented + * r4 twiddle base r5 twiddle displacement in bytes + * r6 pass counter r7 epi3.i, in the low halfword + * r3, r8-r11, ip, lr scratch + * + * Frame, 24 bytes; this kernel parks nothing, so all of it is outer-loop + * state read once per i. + * 0 fstride, bytes 4 Fout for the next i 8 N left + * 12 mm*8 16 M = m*8 20 m + * + * The peeled pass and the twiddled passes share one body: the peel drops B + * and C in straight from memory at full scale, and the twiddled path doubles + * the halved products before falling through to the same code. + * ------------------------------------------------------------------------ */ + .align 2 + .global kf_bfly3_armv5e + .type kf_bfly3_armv5e, %function +kf_bfly3_armv5e: + push {r4-r11, lr} + sub sp, sp, #24 + ldr r6, [sp, #60] @ N + ldr ip, [sp, #64] @ mm + mov r2, r2, lsl #2 @ fstride in bytes + mov ip, ip, lsl #3 @ mm * sizeof(kiss_fft_cpx) + mov lr, r3, lsl #3 @ M = m * sizeof(kiss_fft_cpx) + str r2, [sp, #0] + str r0, [sp, #4] + str r6, [sp, #8] + str ip, [sp, #12] + str lr, [sp, #16] + str r3, [sp, #20] + mov r4, r1 @ twiddle base, invariant + ldr r7, .Lb3_epi3 @ only the low halfword is read by smulwb + +.Lb3_outer: + ldr r0, [sp, #4] @ Fout for this i + ldr r3, [sp, #12] @ mm*8 + ldr ip, [sp, #16] @ M + add r3, r0, r3 + str r3, [sp, #4] @ Fout for i+1 + add r1, r0, ip @ p1 = Fout + m + add r2, r1, ip @ p2 = Fout + 2m + ldr r6, [sp, #20] @ pass counter = m + mov r5, #0 @ twiddle displacement + +/* Pass 0 twiddles by twiddles[0], which is 1: B and C are the stored values + at full scale, so it enters the body with no multiply. */ + ldr r8, [r1] @ B.r + ldr r9, [r1, #4] @ B.i + ldr r10, [r2] @ C.r + ldr r11, [r2, #4] @ C.i + b .Lb3_body + +.Lb3_pass: + ldr r3, [r4, r5] @ tw1, packed + ldr r9, [r1, #4] @ B.i + ldr r8, [r1] @ B.r + CMUL_HALF r8, r9, r3, r10 + ldr r3, [r4, r5, lsl #1] @ tw2, packed + ldr r11, [r2, #4] @ C.i + ldr r10, [r2] @ C.r + CMUL_HALF r10, r11, r3, ip + mov r8, r8, lsl #1 @ B.r + mov r9, r9, lsl #1 @ B.i + mov r10, r10, lsl #1 @ C.r + mov r11, r11, lsl #1 @ C.i + +.Lb3_body: + add r3, r8, r10 @ s3.r = B.r + C.r + sub r8, r8, r10 @ s0.r = B.r - C.r + add ip, r9, r11 @ s3.i + sub r9, r9, r11 @ s0.i + ldr r10, [r0] @ A.r + ldr r11, [r0, #4] @ A.i + sub lr, r10, r3, asr #1 @ A.r - HALF_OF(s3.r) + add r10, r10, r3 @ Fout0.r = A.r + s3.r + str r10, [r0], #4 + sub r3, r11, ip, asr #1 @ A.i - HALF_OF(s3.i) + add r11, r11, ip @ Fout0.i + str r11, [r0], #4 + smulwb r10, r8, r7 @ (s0.r * epi3.i) >> 16 + smulwb r11, r9, r7 @ (s0.i * epi3.i) >> 16 + add ip, lr, r11, lsl #1 @ Fout2.r = h.r + s0'.i + sub r8, r3, r10, lsl #1 @ Fout2.i = h.i - s0'.r + str ip, [r2], #4 + str r8, [r2], #4 + sub ip, lr, r11, lsl #1 @ Fout1.r = h.r - s0'.i + add r8, r3, r10, lsl #1 @ Fout1.i = h.i + s0'.r + str ip, [r1], #4 + str r8, [r1], #4 + ldr r3, [sp, #0] @ fstride, bytes + subs r6, r6, #1 + add r5, r5, r3 @ displacement for the next pass + bne .Lb3_pass + + ldr r3, [sp, #8] @ N left + subs r3, r3, #1 + str r3, [sp, #8] + bne .Lb3_outer + add sp, sp, #24 + pop {r4-r11, pc} +.Lb3_epi3: + .word -28378 + .size kf_bfly3_armv5e, .-kf_bfly3_armv5e + + +/* ------------------------------------------------------------------------ + * void kf_bfly5_armv5e(kiss_fft_cpx *Fout, const kiss_twiddle_cpx *tw, + * int fstride, int m, int N, int mm) + * + * Inner-loop registers: + * r0-r4 the five output pointers, Fout + 0, m, 2m, 3m, 4m, post-incremented + * r5 twiddle base r6 twiddle displacement in bytes + * r7-r11, ip, lr scratch + * + * There is no pass counter: the displacement itself is the induction + * variable, and the loop ends when it reaches m*fstride. That buys back the + * one register this kernel could not otherwise find. + * + * Frame, 56 bytes. The first thirty-two are the placed spill. Five complex + * values will not fit in seven scratch registers at once, so four of them are + * parked at the point where they stop being needed and fetched back one + * component at a time. + * 0 s10.r 4 s10.i 8 s7.r 12 s7.i + * 16 s9.r 20 s9.i 24 s11.r 28 s11.i + * 32 fstride, bytes 36 limit = m*fstride 40 Fout for the next i + * 44 N left 48 mm*8 52 M = m*8 + * + * The eight products that form scratch[6] and scratch[12] all multiply by + * ya.i or yb.i, so both constants live packed in one register and are picked + * apart by smulwb and smulwt. Two of the four sums then collapse into a + * single smulwb and smlawt pair rather than two multiplies, two shifts and + * an add. + * + * The outputs split cleanly: the real parts of all four need only the + * imaginary halves of scratch[9] and scratch[10], and the imaginary parts + * need only the real halves. Doing them in that order halves how much has + * to be live at once. + * ------------------------------------------------------------------------ */ + +/* s7 = P + Q and s10 = P - Q, for P in (r8,r9) and Q in (r10,r11), then park + both. Used twice: once after the twiddled pair, once for the peeled pass. */ + .macro B5_FOLD1 + add r7, r8, r10 @ s7.r + sub r8, r8, r10 @ s10.r + add ip, r9, r11 @ s7.i + sub r9, r9, r11 @ s10.i + str r8, [sp, #0] + str r9, [sp, #4] + str r7, [sp, #8] + str ip, [sp, #12] + .endm + + .align 2 + .global kf_bfly5_armv5e + .type kf_bfly5_armv5e, %function +kf_bfly5_armv5e: + push {r4-r11, lr} + sub sp, sp, #56 + ldr r7, [sp, #92] @ N + ldr ip, [sp, #96] @ mm + mov r2, r2, lsl #2 @ fstride in bytes + mov ip, ip, lsl #3 @ mm * sizeof(kiss_fft_cpx) + mov lr, r3, lsl #3 @ M = m * sizeof(kiss_fft_cpx) + mul r3, r2, r3 @ limit: the displacement after m passes + str r2, [sp, #32] + str r3, [sp, #36] + str r0, [sp, #40] + str r7, [sp, #44] + str ip, [sp, #48] + str lr, [sp, #52] + mov r5, r1 @ twiddle base, invariant + +.Lb5_outer: + ldr r0, [sp, #40] @ Fout for this i + ldr r7, [sp, #48] @ mm*8 + ldr ip, [sp, #52] @ M + add r7, r0, r7 + str r7, [sp, #40] @ Fout for i+1 + add r1, r0, ip @ Fout1 + add r2, r1, ip @ Fout2 + add r3, r2, ip @ Fout3 + add r4, r3, ip @ Fout4 + mov r6, #0 @ twiddle displacement + +/* Pass 0 twiddles by twiddles[0], which is 1, so the four points come in + from memory at full scale with no multiply. */ + ldr r8, [r1] @ s1.r + ldr r9, [r1, #4] @ s1.i + ldr r10, [r4] @ s4.r + ldr r11, [r4, #4] @ s4.i + B5_FOLD1 + ldr r8, [r2] @ s2.r + ldr r9, [r2, #4] @ s2.i + ldr r10, [r3] @ s3.r + ldr r11, [r3, #4] @ s3.i + b .Lb5_fold2 + +.Lb5_pass: + ldr r7, [r5, r6] @ tw1, packed + ldr r9, [r1, #4] @ s1.i + ldr r8, [r1] @ s1.r + CMUL_HALF r8, r9, r7, r10 + ldr r7, [r5, r6, lsl #2] @ tw4, packed + ldr r11, [r4, #4] @ s4.i + ldr r10, [r4] @ s4.r + CMUL_HALF r10, r11, r7, ip + mov r8, r8, lsl #1 @ s1.r + mov r9, r9, lsl #1 @ s1.i + mov r10, r10, lsl #1 @ s4.r + mov r11, r11, lsl #1 @ s4.i + B5_FOLD1 + ldr r7, [r5, r6, lsl #1] @ tw2, packed + ldr r9, [r2, #4] @ s2.i + ldr r8, [r2] @ s2.r + CMUL_HALF r8, r9, r7, r10 + add r7, r6, r6, lsl #1 @ 3 * displacement + ldr r7, [r5, r7] @ tw3, packed + ldr r11, [r3, #4] @ s3.i + ldr r10, [r3] @ s3.r + CMUL_HALF r10, r11, r7, ip + mov r8, r8, lsl #1 @ s2.r + mov r9, r9, lsl #1 @ s2.i + mov r10, r10, lsl #1 @ s3.r + mov r11, r11, lsl #1 @ s3.i + +.Lb5_fold2: + add r7, r8, r10 @ s8.r = s2.r + s3.r + sub r8, r8, r10 @ s9.r + add ip, r9, r11 @ s8.i + sub r9, r9, r11 @ s9.i + str r8, [sp, #16] @ park s9.r + str r9, [sp, #20] @ park s9.i + ldr r10, [sp, #8] @ s7.r + ldr r11, [sp, #12] @ s7.i + add r8, r10, r7 @ p.r = s7.r + s8.r + sub r10, r10, r7 @ q.r + add r9, r11, ip @ p.i + sub r11, r11, ip @ q.i + + ldr r7, [r0] @ A.r + ldr ip, [r0, #4] @ A.i + add lr, r8, #2 + sub lr, r7, lr, asr #2 @ t.r = A.r - QUARTER_OF(p.r) + add r7, r7, r8 @ Fout0.r = A.r + p.r + str r7, [r0], #4 + add r8, r9, #2 + sub r8, ip, r8, asr #2 @ t.i + add ip, ip, r9 @ Fout0.i + str ip, [r0], #4 + + ldr r7, .Lb5_yc + smulwb r9, r10, r7 @ (q.r * yc) >> 16 + smulwb ip, r11, r7 @ (q.i * yc) >> 16 + add r10, lr, r9, lsl #1 @ s5.r = t.r + scratch[4].r + sub r11, lr, r9, lsl #1 @ s11.r = t.r - scratch[4].r + add r7, r8, ip, lsl #1 @ s5.i + sub r9, r8, ip, lsl #1 @ s11.i + str r11, [sp, #24] @ park s11.r + str r9, [sp, #28] @ park s11.i + +/* Real halves. s6.r and s12.r need only the imaginary halves of s9 and + s10, so nothing else has to stay live here. */ + ldr r8, [sp, #4] @ s10.i + ldr r9, [sp, #20] @ s9.i + ldr r11, .Lb5_yab @ ya.i in 15:0, yb.i in 31:16 + smulwb ip, r8, r11 @ s10.i * ya.i + smlawt ip, r9, r11, ip @ + s9.i * yb.i -> s6.r / 2 + smulwb lr, r9, r11 @ s9.i * ya.i + smulwt r9, r8, r11 @ s10.i * yb.i + sub lr, lr, r9 @ -> s12.r / 2 + sub r8, r10, ip, lsl #1 @ Fout1.r = s5.r - s6.r + add r9, r10, ip, lsl #1 @ Fout4.r = s5.r + s6.r + ldr r10, [sp, #24] @ s11.r + str r8, [r1], #4 + str r9, [r4], #4 + add r8, r10, lr, lsl #1 @ Fout2.r = s11.r + s12.r + sub r9, r10, lr, lsl #1 @ Fout3.r + str r8, [r2], #4 + str r9, [r3], #4 + +/* Imaginary halves. s6.i is the negation of the same shape, so the two + stores swap their add and subtract rather than paying for an rsb. */ + ldr r8, [sp, #0] @ s10.r + ldr r9, [sp, #16] @ s9.r + ldr r11, .Lb5_yab + smulwb ip, r8, r11 @ s10.r * ya.i + smlawt ip, r9, r11, ip @ + s9.r * yb.i -> minus s6.i / 2 + smulwt lr, r8, r11 @ s10.r * yb.i + smulwb r9, r9, r11 @ s9.r * ya.i + sub lr, lr, r9 @ -> s12.i / 2 + add r8, r7, ip, lsl #1 @ Fout1.i = s5.i - s6.i + sub r9, r7, ip, lsl #1 @ Fout4.i = s5.i + s6.i + ldr r10, [sp, #28] @ s11.i + str r8, [r1], #4 + str r9, [r4], #4 + add r8, r10, lr, lsl #1 @ Fout2.i = s11.i + s12.i + sub r9, r10, lr, lsl #1 @ Fout3.i + str r8, [r2], #4 + str r9, [r3], #4 + + ldr r7, [sp, #32] @ fstride, bytes + ldr ip, [sp, #36] @ limit + add r6, r6, r7 @ displacement for the next pass + cmp r6, ip + bne .Lb5_pass + + ldr r7, [sp, #44] @ N left + subs r7, r7, #1 + str r7, [sp, #44] + bne .Lb5_outer + add sp, sp, #56 + pop {r4-r11, pc} +.Lb5_yc: + .word 18318 +.Lb5_yab: + .word 0xb4c38644 + .size kf_bfly5_armv5e, .-kf_bfly5_armv5e From 9a972e7f51c56ae467c2de845ced8dbbbce4b1e6 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Thu, 24 Sep 2026 16:56:36 -0400 Subject: [PATCH 225/408] opus: Correct enablement of new ARMv5e optimizations * Only use new ARMv5e optimizations on classic (non-M) profile * fix inconsistent ARM_ARCH >= 5 vs == 5 Fixes red in 0c4345475a and ae223933bf Change-Id: Ieb688679d2d698a19870b1a63b4151a8593e04e0 --- lib/rbcodec/codecs/libopus/SOURCES | 2 +- lib/rbcodec/codecs/libopus/config.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index 279b0f6a6a..16f1caed82 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -14,7 +14,7 @@ celt/arm/mdct_armv4_asm.S celt/arm/comb_filter_armv4_asm.S celt/arm/denorm_armv4_asm.S celt/arm/deemph_armv4_asm.S -#elif defined(CPU_ARM) && (ARM_ARCH == 5) +#elif defined(CPU_ARM) && (ARM_ARCH >= 5) && (ARCH_PROFILE == ARM_PROFILE_CLASSIC) celt/arm/kiss_fft_armv5e_asm.S celt/arm/mdct_armv5e_asm.S celt/arm/comb_filter_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/config.h b/lib/rbcodec/codecs/libopus/config.h index d34ae17ba1..035832125c 100644 --- a/lib/rbcodec/codecs/libopus/config.h +++ b/lib/rbcodec/codecs/libopus/config.h @@ -49,7 +49,7 @@ #define OPUS_ARM_ASM #if ARM_ARCH == 4 #define OPUS_ARM_INLINE_ASM -#elif ARM_ARCH > 4 +#elif (ARM_ARCH > 4) && (ARCH_PROFILE == ARM_PROFILE_CLASSIC) #define OPUS_ARM_INLINE_EDSP #endif From 0918a068eba6e1884dfa967db819d3821c061665 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Thu, 24 Sep 2026 17:07:57 -0400 Subject: [PATCH 226/408] opus: More surgical update to 9a972e7f51c56ae467c2de845ced8dbbbce4b1e6 This re-enables upstream ASM optimizations for Cortex-M. Only the our downstream improvements are disabled, as they do not assemble in thumb2 mode. Change-Id: Icca1b3dbf04786c7714fb4eef92ad66aa55132f3 --- lib/rbcodec/codecs/libopus/config.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/rbcodec/codecs/libopus/config.h b/lib/rbcodec/codecs/libopus/config.h index 035832125c..938e510a4a 100644 --- a/lib/rbcodec/codecs/libopus/config.h +++ b/lib/rbcodec/codecs/libopus/config.h @@ -49,8 +49,12 @@ #define OPUS_ARM_ASM #if ARM_ARCH == 4 #define OPUS_ARM_INLINE_ASM -#elif (ARM_ARCH > 4) && (ARCH_PROFILE == ARM_PROFILE_CLASSIC) +#elif ARM_ARCH > 4 #define OPUS_ARM_INLINE_EDSP +#if (ARCH_PROFILE != ARM_PROFILE_CLASSIC) +#define OPUS_ARM_NO_FFT_ASM +#define OPUS_ARM_NO_MDCT_ASM +#endif #endif /*optimization no hardware division support*/ @@ -65,4 +69,3 @@ #endif #endif /* CONFIG_H */ - From 07557038b48b39f0036fe31fb6b29373de47bdef Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:27 -0400 Subject: [PATCH 227/408] opus: tabulate bitexact_log2tan on PP5022/5024 Its reachable input set is 2,985 (qn,i) pairs for any stream ever, so it tabulates exactly in 6,484 bytes. Verified exhaustively against the compiled functions. Only built for PP5022/PP5024, where the tables fit the 80 KB IRAM window; elsewhere it measured no gain. Measured, e200v1: 39.18 -> 38.96 MHz with the tables in IRAM, 39.07 with them in DRAM. Clip+: 28.05 -> 28.08 MHz, so not built there. Co-Authored-By: Claude Opus 5 Change-Id: I38417793dc337ec6cb61139cc919d23ac09b04dd --- lib/rbcodec/codecs/libopus/celt/bands.c | 29 ++ .../codecs/libopus/celt/log2tan_table.h | 372 ++++++++++++++++++ lib/rbcodec/codecs/libopus/config.h | 7 + 3 files changed, 408 insertions(+) create mode 100644 lib/rbcodec/codecs/libopus/celt/log2tan_table.h diff --git a/lib/rbcodec/codecs/libopus/celt/bands.c b/lib/rbcodec/codecs/libopus/celt/bands.c index eec3dd0f19..54fe0ce7b4 100644 --- a/lib/rbcodec/codecs/libopus/celt/bands.c +++ b/lib/rbcodec/codecs/libopus/celt/bands.c @@ -33,6 +33,7 @@ #include #include "bands.h" +#include "log2tan_table.h" #include "modes.h" #include "vq.h" #include "cwrs.h" @@ -717,6 +718,9 @@ static void compute_theta(struct band_ctx *ctx, struct split_ctx *sctx, { int qn; int itheta=0; +#ifdef OPUS_LOG2TAN_TABLE + int itheta_q=-1; /* the quantiser index behind itheta */ +#endif int delta; int imid, iside; int qalloc; @@ -849,6 +853,9 @@ static void compute_theta(struct band_ctx *ctx, struct split_ctx *sctx, } } celt_assert(itheta>=0); +#ifdef OPUS_LOG2TAN_TABLE + itheta_q = itheta; +#endif itheta = celt_udiv((opus_int32)itheta*16384, qn); if (encode && stereo) { @@ -904,7 +911,29 @@ static void compute_theta(struct band_ctx *ctx, struct split_ctx *sctx, iside = bitexact_cos((opus_int16)(16384-itheta)); /* This is the mid vs side allocation that minimizes squared error in that band. */ +#ifdef OPUS_LOG2TAN_TABLE + /* bitexact_log2tan here is a function of (qn, itheta_q) alone, and that + set is closed and small, so it is tabulated exactly: two EC_ILOG + ladders and four FRAC_MUL16 products become one indexed load. The + bounds test is what makes the lookup safe rather than merely correct: + every decode path bounds itheta_q to [0,qn], but a table index must + not depend on that being true of a corrupt stream. */ + { + /* qn needs no range test: compute_qn clamps qb to 8<inv = inv; diff --git a/lib/rbcodec/codecs/libopus/celt/log2tan_table.h b/lib/rbcodec/codecs/libopus/celt/log2tan_table.h new file mode 100644 index 0000000000..ccbcac8a57 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/log2tan_table.h @@ -0,0 +1,372 @@ +/* Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/kiss_fft.c are met. + */ +/* Generated by an offline script -- do not edit. + * + * bitexact_log2tan(iside,imid) for every (qn,i) the decoder can reach. + * imid is bitexact_cos(itheta) and iside is bitexact_cos(16384-itheta), with + * itheta = celt_udiv(i*16384, qn) and i in [0,qn]; compute_qn yields only the + * 40 values of qn listed below, so this set is closed for any stream. + * + * Both entries are exact, not approximations: the table replaces two EC_ILOG + * ladders and four FRAC_MUL16 products, about 88 ARMv4 cycles, with an + * indexed load. The slots for itheta 0 and 16384 are never read -- those two + * cases return before the call -- but are kept so the index is a plain + * off[qn]+i. + * + * Verified exhaustively against the compiled bitexact_cos and + * bitexact_log2tan by armdecode built with -DVERIFY_LOG2TAN. + */ +#ifndef LOG2TAN_TABLE_H +#define LOG2TAN_TABLE_H + +/* Only built where config.h turns OPUS_LOG2TAN_TABLE on. */ +#ifdef OPUS_LOG2TAN_TABLE + +#define LOG2TAN_QNMAX 256 +#define LOG2TAN_SIZE 2985 + +static const opus_uint16 log2tan_off[LOG2TAN_QNMAX+1] ICONST_ATTR = { + 65535, 0, 2,65535, 5,65535, 10,65535, + 17,65535, 26,65535, 37,65535, 50,65535, + 65,65535, 82,65535, 101,65535, 122,65535, + 145,65535, 170,65535,65535,65535, 197,65535, + 228,65535, 261,65535,65535,65535, 296,65535, + 65535,65535, 335,65535,65535,65535, 378,65535, + 65535,65535, 425,65535,65535,65535, 476,65535, + 65535,65535, 531,65535,65535,65535,65535,65535, + 590,65535,65535,65535,65535,65535, 655,65535, + 65535,65535,65535,65535, 726,65535,65535,65535, + 65535,65535, 803,65535,65535,65535,65535,65535, + 65535,65535, 886,65535,65535,65535,65535,65535, + 65535,65535, 977,65535,65535,65535,65535,65535, + 65535,65535,65535,65535, 1076,65535,65535,65535, + 65535,65535,65535,65535,65535,65535, 1185,65535, + 65535,65535,65535,65535,65535,65535,65535,65535, + 1304,65535,65535,65535,65535,65535,65535,65535, + 65535,65535,65535,65535, 1433,65535,65535,65535, + 65535,65535,65535,65535,65535,65535,65535,65535, + 1574,65535,65535,65535,65535,65535,65535,65535, + 65535,65535,65535,65535,65535,65535, 1727,65535, + 65535,65535,65535,65535,65535,65535,65535,65535, + 65535,65535,65535,65535,65535,65535, 1894,65535, + 65535,65535,65535,65535,65535,65535,65535,65535, + 65535,65535,65535,65535,65535,65535, 2077,65535, + 65535,65535,65535,65535,65535,65535,65535,65535, + 65535,65535,65535,65535,65535,65535,65535,65535, + 2276,65535,65535,65535,65535,65535,65535,65535, + 65535,65535,65535,65535,65535,65535,65535,65535, + 65535,65535, 2493,65535,65535,65535,65535,65535, + 65535,65535,65535,65535,65535,65535,65535,65535, + 65535,65535,65535,65535,65535,65535,65535,65535, + 2728, +}; + +static const opus_int16 log2tan_tab[LOG2TAN_SIZE] ICONST_ATTR = { + 0, 0, 0, 0, 0, 0, -2611, 0, 2611, 0, + 0, -3877, -1605, 0, 1604, 3875, 0, 0, -4769, -2611, + -1195, 0, 1195, 2611, 4769, 0, 0, -5451, -3334, -1993, + -953, 0, 952, 1992, 3332, 5450, 0, 0, -5974, -3877, + -2611, -1605, -793, 0, 793, 1604, 2611, 3875, 5973, 0, + 0, -6441, -4359, -3116, -2159, -1370, -678, 0, 678, 1369, + 2158, 3115, 4358, 6438, 0, 0, -6843, -4769, -3530, -2611, + -1854, -1195, -592, 0, 592, 1195, 1854, 2611, 3530, 4769, + 6843, 0, 0, -7201, -5132, -3877, -2998, -2255, -1605, -1060, + -527, 0, 527, 1060, 1604, 2253, 2998, 3875, 5130, 7198, + 0, 0, -7516, -5451, -4213, -3334, -2611, -1993, -1441, -953, + -474, 0, 474, 952, 1439, 1992, 2611, 3332, 4211, 5450, + 7512, 0, 0, -7793, -5728, -4500, -3624, -2925, -2317, -1793, + -1306, -865, -431, 0, 430, 864, 1306, 1792, 2317, 2924, + 3623, 4499, 5726, 7789, 0, 0, -8037, -5974, -4769, -3877, + -3205, -2611, -2089, -1605, -1195, -793, -394, 0, 395, 793, + 1195, 1604, 2088, 2611, 3204, 3875, 4769, 5973, 8033, 0, + 0, -8283, -6224, -5016, -4136, -3453, -2876, -2361, -1907, -1477, + -1101, -731, -364, 0, 364, 731, 1101, 1477, 1906, 2360, + 2875, 3454, 4136, 5016, 6222, 8279, 0, 0, -8702, -6648, + -5451, -4570, -3877, -3334, -2839, -2393, -1993, -1605, -1276, -953, + -633, -317, 0, 315, 632, 952, 1276, 1604, 1992, 2392, + 2838, 3332, 3875, 4568, 5450, 6644, 8699, 0, 0, -8895, + -6843, -5640, -4769, -4091, -3530, -3049, -2611, -2213, -1854, -1500, + -1195, -892, -592, -296, 0, 296, 592, 892, 1195, 1500, + 1854, 2213, 2611, 3049, 3530, 4091, 4769, 5640, 6843, 8895, + 0, 0, -9085, -7029, -5815, -4957, -4271, -3712, -3243, -2811, + -2419, -2061, -1736, -1411, -1123, -839, -557, -279, 0, 279, + 557, 839, 1124, 1411, 1735, 2060, 2418, 2810, 3241, 3711, + 4269, 4955, 5814, 7025, 9080, 0, 0, -9417, -7364, -6152, + -5297, -4610, -4061, -3585, -3171, -2790, -2438, -2115, -1820, -1518, + -1259, -1003, -750, -499, -250, 0, 249, 498, 749, 1003, + 1259, 1517, 1818, 2114, 2438, 2789, 3171, 3583, 4059, 4609, + 5295, 6150, 7360, 9409, 0, 0, -9709, -7658, -6441, -5594, + -4920, -4359, -3877, -3483, -3116, -2772, -2455, -2159, -1887, -1605, + -1370, -1137, -907, -678, -451, -225, 0, 225, 451, 678, + 905, 1137, 1369, 1604, 1886, 2158, 2453, 2773, 3115, 3482, + 3875, 4358, 4919, 5592, 6438, 7654, 9700, 0, 0, -9967, + -7918, -6715, -5856, -5201, -4636, -4169, -3755, -3401, -3069, -2759, + -2467, -2196, -1943, -1709, -1462, -1248, -1037, -827, -619, -412, + -206, 0, 206, 412, 619, 826, 1035, 1248, 1461, 1708, + 1943, 2195, 2466, 2757, 3068, 3400, 3753, 4167, 4636, 5199, + 5855, 6712, 7914, 9963, 0, 0,-10198, -8145, -6966, -6086, + -5451, -4895, -4421, -4023, -3653, -3334, -3032, -2746, -2478, -2228, + -1993, -1774, -1539, -1342, -1147, -953, -760, -569, -379, -189, + 0, 189, 379, 569, 760, 952, 1146, 1340, 1539, 1772, + 1992, 2228, 2479, 2745, 3031, 3332, 3652, 4022, 4420, 4894, + 5450, 6085, 6963, 8141, 10190, 0, 0,-10444, -8396, -7201, + -6336, -5675, -5132, -4658, -4248, -3877, -3568, -3276, -2998, -2735, + -2488, -2255, -2035, -1830, -1605, -1422, -1240, -1060, -882, -704, + -527, -350, -176, 0, 174, 351, 527, 703, 881, 1060, + 1239, 1421, 1604, 1829, 2034, 2253, 2487, 2736, 2998, 3275, + 3567, 3875, 4247, 4657, 5130, 5673, 6333, 7198, 8389, 10433, + 0, 0,-10658, -8606, -7416, -6547, -5880, -5350, -4879, -4466, + -4112, -3780, -3496, -3228, -2970, -2727, -2496, -2279, -2071, -1878, + -1693, -1491, -1321, -1153, -985, -820, -655, -490, -326, -164, + 0, 163, 326, 490, 654, 820, 985, 1152, 1321, 1491, + 1693, 1877, 2071, 2278, 2496, 2726, 2969, 3226, 3495, 3779, + 4110, 4465, 4876, 5348, 5879, 6545, 7413, 8601, 10648, 0, + 0,-10947, -8895, -7703, -6843, -6180, -5640, -5180, -4769, -4406, + -4091, -3788, -3530, -3284, -3049, -2824, -2611, -2406, -2213, -2028, + -1854, -1687, -1500, -1347, -1195, -1043, -892, -743, -592, -444, + -296, -148, 0, 148, 296, 444, 592, 743, 892, 1043, + 1195, 1347, 1500, 1687, 1854, 2028, 2213, 2406, 2611, 2824, + 3049, 3284, 3530, 3788, 4091, 4406, 4769, 5180, 5640, 6180, + 6843, 7703, 8895, 10947, 0, 0,-11216, -9172, -7957, -7116, + -6441, -5896, -5451, -5047, -4683, -4359, -4075, -3796, -3560, -3334, + -3116, -2907, -2707, -2516, -2333, -2159, -1993, -1834, -1683, -1511, + -1370, -1230, -1090, -953, -815, -678, -542, -407, -271, -136, + 0, 135, 270, 406, 541, 678, 815, 952, 1090, 1229, + 1369, 1511, 1683, 1834, 1992, 2158, 2332, 2515, 2707, 2906, + 3115, 3332, 3559, 3795, 4074, 4358, 4680, 5045, 5450, 5894, + 6438, 7111, 7952, 9164, 11203, 0, 0,-11475, -9417, -8214, + -7364, -6689, -6152, -5692, -5297, -4936, -4610, -4319, -4061, -3802, + -3585, -3374, -3171, -2978, -2790, -2611, -2438, -2273, -2115, -1964, + -1820, -1680, -1518, -1388, -1259, -1131, -1003, -877, -750, -624, + -499, -373, -250, -124, 0, 124, 249, 373, 498, 623, + 749, 876, 1003, 1130, 1259, 1388, 1517, 1679, 1818, 1962, + 2114, 2272, 2438, 2611, 2789, 2977, 3171, 3374, 3583, 3801, + 4059, 4318, 4609, 4935, 5295, 5689, 6150, 6686, 7360, 8209, + 9409, 11461, 0, 0,-11700, -9644, -8427, -7588, -6918, -6371, + -5909, -5523, -5170, -4845, -4551, -4286, -4049, -3808, -3604, -3410, + -3219, -3038, -2861, -2693, -2530, -2373, -2222, -2078, -1937, -1805, + -1677, -1525, -1403, -1284, -1165, -1047, -929, -812, -695, -579, + -462, -347, -232, -116, 0, 115, 231, 346, 462, 577, + 694, 811, 929, 1046, 1166, 1283, 1404, 1523, 1676, 1804, + 1937, 2077, 2221, 2372, 2529, 2692, 2860, 3037, 3220, 3409, + 3605, 3807, 4048, 4284, 4550, 4845, 5169, 5522, 5906, 6369, + 6915, 7584, 8421, 9637, 11686, 0, 0,-11954, -9903, -8702, + -7855, -7201, -6648, -6194, -5794, -5451, -5132, -4840, -4570, -4325, + -4104, -3877, -3690, -3508, -3334, -3163, -2998, -2839, -2686, -2536, + -2393, -2255, -2123, -1993, -1869, -1750, -1605, -1494, -1386, -1276, + -1168, -1060, -953, -846, -739, -633, -527, -421, -317, -211, + -106, 0, 105, 210, 315, 421, 527, 632, 739, 845, + 952, 1060, 1167, 1276, 1385, 1495, 1604, 1749, 1869, 1992, + 2120, 2253, 2392, 2536, 2685, 2838, 2998, 3162, 3332, 3508, + 3689, 3875, 4103, 4324, 4568, 4838, 5130, 5450, 5792, 6192, + 6644, 7198, 7850, 8699, 9896, 11939, 0, 0,-12190,-10146, + -8962, -8091, -7458, -6906, -6441, -6030, -5701, -5390, -5101, -4833, + -4586, -4359, -4152, -3936, -3762, -3592, -3428, -3270, -3116, -2965, + -2820, -2679, -2543, -2410, -2283, -2159, -2039, -1924, -1813, -1705, + -1572, -1470, -1370, -1269, -1170, -1072, -972, -874, -776, -678, + -581, -483, -387, -289, -193, -97, 0, 97, 193, 290, + 386, 483, 581, 678, 776, 873, 971, 1071, 1169, 1269, + 1369, 1469, 1570, 1704, 1812, 1925, 2040, 2158, 2282, 2409, + 2542, 2678, 2820, 2964, 3115, 3269, 3429, 3591, 3760, 3935, + 4151, 4358, 4584, 4832, 5099, 5389, 5698, 6029, 6438, 6903, + 7455, 8089, 8956, 10136, 12170, 0, 0,-12510,-10444, -9254, + -8396, -7740, -7201, -6734, -6336, -5974, -5675, -5397, -5132, -4886, + -4658, -4445, -4248, -4070, -3877, -3721, -3568, -3421, -3276, -3134, + -2998, -2865, -2735, -2611, -2488, -2370, -2255, -2143, -2035, -1930, + -1830, -1730, -1605, -1513, -1422, -1331, -1240, -1149, -1060, -969, + -882, -793, -704, -614, -527, -439, -350, -264, -176, -88, + 0, 87, 174, 263, 351, 439, 527, 615, 703, 793, + 881, 970, 1060, 1150, 1239, 1331, 1421, 1512, 1604, 1730, + 1829, 1929, 2034, 2143, 2253, 2369, 2487, 2611, 2736, 2864, + 2998, 3134, 3275, 3420, 3567, 3720, 3875, 4068, 4247, 4444, + 4657, 4884, 5130, 5395, 5673, 5973, 6333, 6731, 7198, 7736, + 8389, 9248, 10433, 12481, 0, 0,-12765,-10706, -9520, -8653, + -7989, -7465, -7004, -6598, -6251, -5928, -5658, -5400, -5159, -4931, + -4718, -4518, -4332, -4162, -3975, -3829, -3687, -3548, -3412, -3280, + -3151, -3026, -2904, -2784, -2667, -2553, -2443, -2337, -2231, -2131, + -2032, -1935, -1842, -1752, -1663, -1549, -1465, -1381, -1299, -1216, + -1132, -1051, -970, -888, -806, -725, -643, -564, -483, -401, + -321, -242, -161, -80, 0, 79, 160, 240, 321, 402, + 482, 562, 643, 724, 805, 886, 969, 1051, 1132, 1215, + 1297, 1380, 1464, 1548, 1663, 1751, 1842, 1936, 2032, 2130, + 2232, 2335, 2443, 2553, 2666, 2783, 2902, 3024, 3150, 3279, + 3412, 3548, 3686, 3828, 3974, 4160, 4331, 4517, 4717, 4930, + 5157, 5399, 5656, 5926, 6249, 6596, 7001, 7461, 7984, 8648, + 9514, 10696, 12745, 0, 0,-12988,-10947, -9754, -8895, -8239, + -7703, -7248, -6843, -6487, -6180, -5888, -5640, -5404, -5180, -4968, + -4769, -4581, -4406, -4242, -4091, -3921, -3788, -3658, -3530, -3406, + -3284, -3165, -3049, -2936, -2824, -2716, -2611, -2507, -2406, -2308, + -2213, -2119, -2028, -1940, -1854, -1769, -1687, -1579, -1500, -1424, + -1347, -1271, -1195, -1119, -1043, -968, -892, -817, -743, -668, + -592, -519, -444, -369, -296, -222, -148, -74, 0, 74, + 148, 222, 296, 369, 444, 519, 592, 668, 743, 817, + 892, 968, 1043, 1119, 1195, 1271, 1347, 1424, 1500, 1579, + 1687, 1769, 1854, 1940, 2028, 2119, 2213, 2308, 2406, 2507, + 2611, 2716, 2824, 2936, 3049, 3165, 3284, 3406, 3530, 3658, + 3788, 3921, 4091, 4242, 4406, 4581, 4769, 4968, 5180, 5404, + 5640, 5888, 6180, 6487, 6843, 7248, 7703, 8239, 8895, 9754, + 10947, 12988, 0, 0,-13272,-11216,-10005, -9172, -8497, -7957, + -7516, -7116, -6757, -6441, -6166, -5896, -5668, -5451, -5244, -5047, + -4858, -4683, -4516, -4359, -4213, -4075, -3918, -3796, -3677, -3560, + -3446, -3334, -3223, -3116, -3009, -2907, -2807, -2707, -2611, -2516, + -2424, -2333, -2245, -2159, -2075, -1993, -1912, -1834, -1758, -1683, + -1581, -1511, -1441, -1370, -1300, -1230, -1161, -1090, -1022, -953, + -883, -815, -747, -678, -611, -542, -474, -407, -338, -271, + -203, -136, -68, 0, 68, 135, 204, 270, 339, 406, + 474, 541, 610, 678, 746, 815, 883, 952, 1022, 1090, + 1160, 1229, 1299, 1369, 1439, 1511, 1581, 1683, 1758, 1834, + 1911, 1992, 2073, 2158, 2244, 2332, 2423, 2515, 2611, 2707, + 2804, 2906, 3009, 3115, 3222, 3332, 3445, 3559, 3676, 3795, + 3917, 4074, 4211, 4358, 4515, 4680, 4857, 5045, 5242, 5450, + 5667, 5894, 6164, 6438, 6755, 7111, 7512, 7952, 8493, 9164, + 9997, 11203, 13247, 0, 0,-13536,-11475,-10265, -9417, -8746, + -8214, -7756, -7364, -7007, -6689, -6404, -6152, -5902, -5692, -5492, + -5297, -5112, -4936, -4769, -4610, -4461, -4319, -4185, -4061, -3915, + -3802, -3692, -3585, -3479, -3374, -3272, -3171, -3072, -2978, -2883, + -2790, -2698, -2611, -2524, -2438, -2355, -2273, -2193, -2115, -2038, + -1964, -1889, -1820, -1749, -1680, -1582, -1518, -1453, -1388, -1323, + -1259, -1195, -1131, -1068, -1003, -940, -877, -813, -750, -687, + -624, -562, -499, -437, -373, -311, -250, -187, -124, -63, + 0, 62, 124, 187, 249, 310, 373, 436, 498, 562, + 623, 686, 749, 813, 876, 940, 1003, 1066, 1130, 1195, + 1259, 1323, 1388, 1452, 1517, 1583, 1679, 1748, 1818, 1888, + 1962, 2037, 2114, 2192, 2272, 2355, 2438, 2522, 2611, 2698, + 2789, 2882, 2977, 3072, 3171, 3271, 3374, 3478, 3583, 3691, + 3801, 3914, 4059, 4184, 4318, 4459, 4609, 4769, 4935, 5111, + 5295, 5489, 5689, 5900, 6150, 6400, 6686, 7004, 7360, 7753, + 8209, 8739, 9409, 10254, 11461, 13509, 0, 0,-13791,-11728, + -10515, -9679, -9012, -8461, -8003, -7624, -7274, -6958, -6667, -6407, + -6174, -5942, -5747, -5560, -5380, -5207, -5041, -4884, -4733, -4589, + -4452, -4323, -4200, -4084, -3946, -3844, -3741, -3641, -3543, -3446, + -3352, -3259, -3167, -3076, -2988, -2901, -2817, -2734, -2651, -2570, + -2491, -2413, -2338, -2263, -2191, -2119, -2048, -1979, -1911, -1846, + -1781, -1718, -1656, -1566, -1507, -1446, -1386, -1327, -1269, -1209, + -1151, -1092, -1034, -976, -918, -859, -802, -744, -686, -630, + -572, -514, -457, -400, -342, -285, -228, -172, -114, -58, + 0, 56, 113, 170, 228, 285, 342, 399, 456, 514, + 571, 628, 686, 744, 801, 859, 917, 975, 1034, 1092, + 1149, 1209, 1267, 1327, 1386, 1445, 1505, 1564, 1655, 1717, + 1779, 1844, 1912, 1980, 2047, 2117, 2191, 2262, 2338, 2413, + 2490, 2569, 2650, 2732, 2815, 2901, 2989, 3076, 3165, 3257, + 3351, 3446, 3542, 3640, 3740, 3843, 3945, 4082, 4198, 4321, + 4450, 4587, 4732, 4882, 5040, 5206, 5380, 5558, 5745, 5940, + 6172, 6404, 6664, 6955, 7271, 7620, 8000, 8457, 9007, 9672, + 10506, 11714, 13754, 0, 0,-14032,-11983,-10787, -9940, -9285, + -8736, -8283, -7885, -7549, -7235, -6945, -6680, -6441, -6224, -6005, + -5825, -5651, -5482, -5322, -5167, -5016, -4874, -4735, -4605, -4479, + -4359, -4245, -4136, -4034, -3909, -3815, -3722, -3632, -3542, -3453, + -3367, -3282, -3198, -3116, -3034, -2955, -2876, -2799, -2723, -2648, + -2573, -2502, -2431, -2361, -2292, -2225, -2159, -2094, -2031, -1968, + -1907, -1846, -1788, -1729, -1672, -1587, -1532, -1477, -1423, -1370, + -1316, -1261, -1208, -1155, -1101, -1048, -995, -942, -889, -836, + -784, -731, -678, -627, -574, -521, -469, -416, -364, -313, + -260, -209, -157, -105, -53, 0, 52, 103, 156, 208, + 260, 312, 364, 416, 468, 521, 574, 626, 678, 731, + 783, 836, 888, 942, 995, 1048, 1101, 1154, 1207, 1262, + 1315, 1369, 1423, 1477, 1531, 1587, 1672, 1729, 1787, 1846, + 1906, 1966, 2029, 2093, 2158, 2225, 2292, 2360, 2430, 2501, + 2572, 2646, 2721, 2798, 2875, 2954, 3033, 3115, 3197, 3281, + 3367, 3454, 3541, 3631, 3721, 3814, 3908, 4033, 4136, 4243, + 4358, 4478, 4603, 4735, 4872, 5016, 5165, 5321, 5481, 5649, + 5823, 6003, 6222, 6438, 6677, 6942, 7231, 7545, 7881, 8279, + 8730, 9279, 9933, 10772, 11968, 14003, 0, 0,-14286,-12221, + -11043,-10174, -9543, -8993, -8528, -8121, -7793, -7485, -7201, -6939, + -6696, -6473, -6268, -6060, -5892, -5728, -5572, -5423, -5275, -5132, + -4996, -4866, -4739, -4617, -4500, -4390, -4283, -4182, -4084, -3965, + -3877, -3792, -3706, -3624, -3541, -3460, -3380, -3302, -3224, -3148, + -3072, -2998, -2925, -2854, -2781, -2714, -2645, -2577, -2510, -2445, + -2381, -2317, -2255, -2194, -2134, -2073, -2015, -1960, -1902, -1847, + -1793, -1739, -1686, -1605, -1555, -1505, -1455, -1405, -1355, -1306, + -1256, -1207, -1158, -1109, -1060, -1011, -962, -915, -865, -816, + -768, -719, -671, -623, -574, -527, -479, -431, -383, -335, + -287, -239, -191, -143, -96, -48, 0, 48, 96, 144, + 190, 238, 286, 335, 382, 430, 478, 527, 574, 623, + 671, 720, 767, 815, 864, 914, 962, 1011, 1060, 1109, + 1157, 1207, 1256, 1306, 1355, 1405, 1454, 1505, 1553, 1604, + 1686, 1739, 1792, 1846, 1902, 1959, 2015, 2074, 2133, 2193, + 2253, 2317, 2380, 2444, 2509, 2576, 2643, 2712, 2781, 2852, + 2924, 2998, 3072, 3147, 3223, 3301, 3379, 3459, 3540, 3623, + 3705, 3790, 3875, 3963, 4084, 4181, 4282, 4388, 4499, 4616, + 4737, 4864, 4995, 5130, 5274, 5421, 5570, 5726, 5890, 6058, + 6266, 6469, 6691, 6936, 7198, 7483, 7789, 8116, 8521, 8987, + 9534, 10167, 11031, 12205, 14254, 0, 0,-14566,-12510,-11311, + -10444, -9790, -9254, -8791, -8396, -8037, -7740, -7461, -7201, -6958, + -6734, -6525, -6336, -6160, -5974, -5823, -5675, -5535, -5397, -5262, + -5132, -5007, -4886, -4769, -4658, -4549, -4445, -4345, -4248, -4156, + -4070, -3958, -3877, -3798, -3721, -3644, -3568, -3495, -3421, -3347, + -3276, -3205, -3134, -3066, -2998, -2932, -2865, -2799, -2735, -2673, + -2611, -2549, -2488, -2429, -2370, -2312, -2255, -2199, -2143, -2089, + -2035, -1983, -1930, -1879, -1830, -1779, -1730, -1682, -1605, -1559, + -1513, -1468, -1422, -1377, -1331, -1286, -1240, -1195, -1149, -1106, + -1060, -1016, -969, -926, -882, -837, -793, -748, -704, -660, + -614, -572, -527, -483, -439, -394, -350, -308, -264, -219, + -176, -132, -88, -44, 0, 44, 87, 132, 174, 219, + 263, 306, 351, 395, 439, 483, 527, 571, 615, 660, + 703, 747, 793, 836, 881, 925, 970, 1015, 1060, 1104, + 1150, 1195, 1239, 1284, 1331, 1375, 1421, 1467, 1512, 1558, + 1604, 1682, 1730, 1778, 1829, 1878, 1929, 1982, 2034, 2088, + 2143, 2198, 2253, 2311, 2369, 2428, 2487, 2549, 2611, 2671, + 2736, 2800, 2864, 2931, 2998, 3065, 3134, 3204, 3275, 3347, + 3420, 3494, 3567, 3644, 3720, 3797, 3875, 3957, 4068, 4155, + 4247, 4343, 4444, 4548, 4657, 4769, 4884, 5005, 5130, 5261, + 5395, 5533, 5673, 5822, 5973, 6158, 6333, 6522, 6731, 6955, + 7198, 7458, 7736, 8033, 8389, 8785, 9248, 9783, 10433, 11299, + 12481, 14530, 0, 0,-14773,-12718,-11536,-10679,-10015, -9494, + -9028, -8626, -8283, -7965, -7694, -7440, -7201, -6975, -6767, -6570, + -6391, -6224, -6045, -5903, -5767, -5633, -5502, -5375, -5253, -5132, + -5016, -4905, -4795, -4690, -4591, -4493, -4397, -4307, -4221, -4136, + -4057, -3951, -3877, -3805, -3734, -3661, -3591, -3522, -3453, -3387, + -3320, -3255, -3189, -3124, -3062, -2998, -2938, -2876, -2815, -2755, + -2698, -2638, -2582, -2526, -2470, -2415, -2361, -2307, -2255, -2204, + -2152, -2102, -2052, -2002, -1954, -1907, -1860, -1813, -1768, -1723, + -1679, -1605, -1562, -1520, -1477, -1436, -1394, -1352, -1310, -1267, + -1226, -1185, -1143, -1101, -1060, -1019, -978, -937, -894, -854, + -813, -772, -731, -690, -650, -608, -567, -527, -487, -446, + -405, -364, -325, -284, -243, -204, -162, -122, -82, -42, + 0, 40, 81, 121, 161, 202, 243, 283, 323, 364, + 405, 446, 486, 527, 567, 608, 648, 689, 731, 771, + 812, 854, 894, 935, 977, 1019, 1060, 1101, 1143, 1184, + 1226, 1267, 1309, 1351, 1393, 1435, 1477, 1520, 1562, 1604, + 1678, 1722, 1766, 1813, 1859, 1906, 1954, 2002, 2051, 2100, + 2151, 2202, 2253, 2307, 2360, 2413, 2469, 2524, 2580, 2638, + 2697, 2755, 2815, 2875, 2936, 2998, 3061, 3124, 3187, 3254, + 3319, 3386, 3454, 3521, 3591, 3661, 3732, 3803, 3875, 3950, + 4056, 4136, 4219, 4306, 4396, 4491, 4589, 4690, 4794, 4903, + 5016, 5130, 5250, 5373, 5500, 5631, 5765, 5902, 6043, 6222, + 6387, 6568, 6763, 6972, 7198, 7437, 7690, 7961, 8279, 8621, + 9022, 9487, 10005, 10669, 11523, 12697, 14732, 0, 0,-15059, + -12988,-11803,-10947,-10290, -9754, -9300, -8895, -8543, -8239, -7949, + -7703, -7469, -7248, -7039, -6843, -6660, -6487, -6329, -6180, -6019, + -5888, -5763, -5640, -5521, -5404, -5291, -5180, -5073, -4968, -4868, + -4769, -4674, -4581, -4493, -4406, -4323, -4242, -4165, -4091, -4019, + -3921, -3854, -3788, -3722, -3658, -3593, -3530, -3468, -3406, -3345, + -3284, -3224, -3165, -3108, -3049, -2991, -2936, -2878, -2824, -2770, + -2716, -2663, -2611, -2558, -2507, -2456, -2406, -2357, -2308, -2260, + -2213, -2165, -2119, -2074, -2028, -1984, -1940, -1896, -1854, -1810, + -1769, -1729, -1687, -1648, -1579, -1540, -1500, -1463, -1424, -1386, + -1347, -1309, -1271, -1233, -1195, -1157, -1119, -1081, -1043, -1005, + -968, -930, -892, -855, -817, -780, -743, -705, -668, -630, + -592, -556, -519, -481, -444, -407, -369, -333, -296, -258, + -222, -185, -148, -111, -74, -36, 0, 36, 74, 111, + 148, 185, 222, 258, 296, 333, 369, 407, 444, 481, + 519, 556, 592, 630, 668, 705, 743, 780, 817, 855, + 892, 930, 968, 1005, 1043, 1081, 1119, 1157, 1195, 1233, + 1271, 1309, 1347, 1386, 1424, 1463, 1500, 1540, 1579, 1648, + 1687, 1729, 1769, 1810, 1854, 1896, 1940, 1984, 2028, 2074, + 2119, 2165, 2213, 2260, 2308, 2357, 2406, 2456, 2507, 2558, + 2611, 2663, 2716, 2770, 2824, 2878, 2936, 2991, 3049, 3108, + 3165, 3224, 3284, 3345, 3406, 3468, 3530, 3593, 3658, 3722, + 3788, 3854, 3921, 4019, 4091, 4165, 4242, 4323, 4406, 4493, + 4581, 4674, 4769, 4868, 4968, 5073, 5180, 5291, 5404, 5521, + 5640, 5763, 5888, 6019, 6180, 6329, 6487, 6660, 6843, 7039, + 7248, 7469, 7703, 7949, 8239, 8543, 8895, 9300, 9754, 10290, + 10947, 11803, 12988, 15059, 0, +}; + +#endif /* OPUS_LOG2TAN_TABLE */ + +#endif /* LOG2TAN_TABLE_H */ diff --git a/lib/rbcodec/codecs/libopus/config.h b/lib/rbcodec/codecs/libopus/config.h index 938e510a4a..43a94d7b0f 100644 --- a/lib/rbcodec/codecs/libopus/config.h +++ b/lib/rbcodec/codecs/libopus/config.h @@ -62,6 +62,13 @@ #define OPUS_EC_DECODE_DIV #endif +/* Exact lookup table for bitexact_log2tan. Saves about 0.25 MHz if the + tables can live in IRAM, which only the ARMv4 PP5022/PP5024 have room for, + and nothing otherwise, so it is only built there. */ +#if CONFIG_CPU == PP5022 || CONFIG_CPU == PP5024 +#define OPUS_LOG2TAN_TABLE +#endif + #endif #if defined(CPU_COLDFIRE) From ff762858c79880178cc1d012f91d52931eab2d09 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:28 -0400 Subject: [PATCH 228/408] opus: build the decoder without the encoder halves Rockbox lists no encoder, so CELT_DECODE_ONLY folds away the thirteen encoder branches in celt/bands.c and stops gcc keeping their values live across quant_partition's recursive calls. 2,976 bytes smaller. Modelled: -0.47% ARMv4, -0.69% ARMv5E. Co-Authored-By: Claude Opus 5 Change-Id: Ie96a0a63e035ab0cdcdbe9eeeb6691f24fecfa41 --- lib/rbcodec/codecs/libopus/celt/bands.c | 23 ++++++++++++++----- .../codecs/libopus/celt/celt_encoder.c | 4 ++++ lib/rbcodec/codecs/libopus/config.h | 10 ++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/lib/rbcodec/codecs/libopus/celt/bands.c b/lib/rbcodec/codecs/libopus/celt/bands.c index 54fe0ce7b4..9b21dfcaf4 100644 --- a/lib/rbcodec/codecs/libopus/celt/bands.c +++ b/lib/rbcodec/codecs/libopus/celt/bands.c @@ -34,6 +34,17 @@ #include #include "bands.h" #include "log2tan_table.h" + +/* Rockbox builds the decoder only: celt_encoder.c is not in SOURCES, and the + one caller of quant_all_bands passes encode=0. Saying so lets gcc fold the + thirteen encoder-only branches in this file away -- and, worth more than the + branches, stops it keeping their live values in registers across the + recursive calls, which is where quant_partition was spilling. */ +#ifdef CELT_DECODE_ONLY +# define CELT_ENCODE_FLAG(x) 0 +#else +# define CELT_ENCODE_FLAG(x) (x) +#endif #include "modes.h" #include "vq.h" #include "cwrs.h" @@ -735,7 +746,7 @@ static void compute_theta(struct band_ctx *ctx, struct split_ctx *sctx, ec_ctx *ec; const celt_ener *bandE; - encode = ctx->encode; + encode = CELT_ENCODE_FLAG(ctx->encode); m = ctx->m; i = ctx->i; intensity = ctx->intensity; @@ -954,7 +965,7 @@ static unsigned quant_band_n1(struct band_ctx *ctx, celt_norm *X, celt_norm *Y, (void)b; - encode = ctx->encode; + encode = CELT_ENCODE_FLAG(ctx->encode); ec = ctx->ec; stereo = Y != NULL; @@ -1004,7 +1015,7 @@ static unsigned quant_partition(struct band_ctx *ctx, celt_norm *X, int spread; ec_ctx *ec; - encode = ctx->encode; + encode = CELT_ENCODE_FLAG(ctx->encode); m = ctx->m; i = ctx->i; spread = ctx->spread; @@ -1168,7 +1179,7 @@ static unsigned quant_band(struct band_ctx *ctx, celt_norm *X, int encode; int tf_change; - encode = ctx->encode; + encode = CELT_ENCODE_FLAG(ctx->encode); tf_change = ctx->tf_change; longBlocks = B0==1; @@ -1294,7 +1305,7 @@ static unsigned quant_band_stereo(struct band_ctx *ctx, celt_norm *X, celt_norm int encode; ec_ctx *ec; - encode = ctx->encode; + encode = CELT_ENCODE_FLAG(ctx->encode); ec = ctx->ec; /* Special case for one sample */ @@ -1503,7 +1514,7 @@ void quant_all_bands(int encode, const CELTMode *m, int start, int end, lowband_offset = 0; ctx.bandE = bandE; ctx.ec = ec; - + encode = CELT_ENCODE_FLAG(encode); ctx.encode = encode; ctx.intensity = intensity; ctx.m = m; diff --git a/lib/rbcodec/codecs/libopus/celt/celt_encoder.c b/lib/rbcodec/codecs/libopus/celt/celt_encoder.c index 44cb0850ab..4c5fdaf53c 100644 --- a/lib/rbcodec/codecs/libopus/celt/celt_encoder.c +++ b/lib/rbcodec/codecs/libopus/celt/celt_encoder.c @@ -33,6 +33,10 @@ #define CELT_ENCODER_C +#if defined(CELT_DECODE_ONLY) +#error "CELT_DECODE_ONLY is set in libopus/config.h because SOURCES lists no encoder. Building celt_encoder.c means that is no longer true: remove the define, or the encoder halves of celt/bands.c will have been compiled out." +#endif + #include "cpu_support.h" #include "os_support.h" #include "mdct.h" diff --git a/lib/rbcodec/codecs/libopus/config.h b/lib/rbcodec/codecs/libopus/config.h index 43a94d7b0f..b45aa2e8a0 100644 --- a/lib/rbcodec/codecs/libopus/config.h +++ b/lib/rbcodec/codecs/libopus/config.h @@ -13,6 +13,16 @@ /* general stuff */ #define OPUS_BUILD +/* Rockbox decodes Opus and never encodes it: neither celt_encoder.c nor + opus_encoder.c is listed in SOURCES. Saying so lets gcc fold away the + encoder halves of the routines celt/bands.c shares between the two + directions, which is worth 2,976 bytes of code and, because those branches + were holding values live across quant_partition's recursive calls, a + measurable amount of its stack spill. celt_encoder.c carries an #error + against this define, so adding an encoder back fails the build loudly + rather than miscompiling. */ +#define CELT_DECODE_ONLY + /* alloc stuff */ #define VAR_ARRAYS #define NORM_ALIASING_HACK From 2b664d6025e34970f3c58c05f90b0c086dd25e44 Mon Sep 17 00:00:00 2001 From: Paul Sauro Date: Mon, 21 Sep 2026 00:17:29 +0200 Subject: [PATCH 229/408] usb: preserve exclusive storage ownership across repeated configuration and reset Do not release and reacquire the disk when a host repeats configuration or resets the bus before selecting mass storage again. This avoids remounting local filesystems while the host still owns the disk. After reset the filesystems remain unmounted until the host selects a configuration which releases storage, or the cable is unplugged. A host normally reconfigures promptly; one that never does leaves the disk unavailable locally until unplug, rather than risking simultaneous access. Change-Id: I6e296247dd2227e549105d85760613c1a81554a1 --- firmware/usb.c | 8 ++++++++ firmware/usbstack/usb_core.c | 16 +++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/firmware/usb.c b/firmware/usb.c index b41facb39d..080bc50334 100644 --- a/firmware/usb.c +++ b/firmware/usb.c @@ -862,10 +862,17 @@ bool usb_exclusive_storage(void) void usb_request_exclusive_storage(void) { + if(exclusive_storage_requested) + return; + exclusive_storage_requested = true; usb_broadcast_seqnum += 1; usb_num_acks_to_expect = queue_broadcast(SYS_USB_CONNECTED, usb_broadcast_seqnum) - 1; DEBUGF("usb: waiting for %d acks...\n", usb_num_acks_to_expect); + if(usb_num_acks_to_expect == 0 && usb_host_present) { + usb_slave_mode(true); + exclusive_storage_enabled = true; + } } void usb_release_exclusive_storage(void) @@ -875,6 +882,7 @@ void usb_release_exclusive_storage(void) return; } exclusive_storage_requested = false; + usb_num_acks_to_expect = 0; if(exclusive_storage_enabled) { usb_slave_mode(false); diff --git a/firmware/usbstack/usb_core.c b/firmware/usbstack/usb_core.c index ca84122341..69dbf1f3e5 100644 --- a/firmware/usbstack/usb_core.c +++ b/firmware/usbstack/usb_core.c @@ -922,13 +922,18 @@ static int usb_core_do_set_config(uint8_t new_config) } if(require_exclusive) { - if(!usb_exclusive_storage()) { - usb_release_exclusive_storage(); - usb_request_exclusive_storage(); - } - } else { + /* Also preserve a pending handover across repeated SET_CONFIG. + * Restarting it discards acknowledgements and races the USB UI. */ + usb_request_exclusive_storage(); + } else if(!bus_reset_pending) { usb_release_exclusive_storage(); } + /* A bus reset is not a physical disconnect. Keep the storage handover + * (and its acknowledgement epoch) until the host selects a new config + * or the cable is removed; remounting here races re-enumeration. + * If the host never sends SET_CONFIGURATION again, local storage stays + * unavailable until unplug. We expect reconfiguration promptly after a + * reset, but deliberately do not remount on a timer. */ if(require_cpu_boost) { trigger_cpu_boost(); thread_set_priority(thread_self(), PRIORITY_REALTIME); @@ -937,6 +942,7 @@ static int usb_core_do_set_config(uint8_t new_config) cancel_cpu_boost(); } + #ifdef HAVE_USB_CHARGING_ENABLE usb_charging_maxcurrent_change(usb_charging_maxcurrent()); #endif From e2ee665ccea91cb84d44339617aed6759833adca Mon Sep 17 00:00:00 2001 From: Paul Sauro Date: Mon, 21 Sep 2026 00:17:29 +0200 Subject: [PATCH 230/408] usb storage: defer commands until exclusive storage handover completes A host may send a command after SET_CONFIGURATION while filesystem clients are still acknowledging the storage handover. Previously TEST UNIT READY could report no medium, while other commands could reach storage before exclusive access was granted. Retain the first CBW until exclusive access is available. Notify the mass-storage class when handover completes, then execute the retained command and keep the OUT endpoint unarmed until its CSW. On BOT reset, discard the retained CBW and accept a new command. This changes command handling while waiting for ownership. The policy for requesting, preserving and releasing exclusive storage is handled by a separate preparatory patch. Change-Id: If82fc87160e9d7da930a3217445fea92e58dd1ec --- firmware/usb.c | 6 +++++ firmware/usbstack/usb_storage.c | 40 ++++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/firmware/usb.c b/firmware/usb.c index 080bc50334..3b73f8ec34 100644 --- a/firmware/usb.c +++ b/firmware/usb.c @@ -504,6 +504,9 @@ static void NORETURN_ATTR usb_thread(void) if(usb_host_present && exclusive_storage_requested) { usb_slave_mode(true); exclusive_storage_enabled = true; +#ifdef USB_ENABLE_STORAGE + usb_signal_class_notify(USB_DRIVER_MASS_STORAGE, 0); +#endif } break; /* SYS_USB_CONNECTED_ACK */ @@ -872,6 +875,9 @@ void usb_request_exclusive_storage(void) if(usb_num_acks_to_expect == 0 && usb_host_present) { usb_slave_mode(true); exclusive_storage_enabled = true; +#ifdef USB_ENABLE_STORAGE + usb_signal_class_notify(USB_DRIVER_MASS_STORAGE, 0); +#endif } } diff --git a/firmware/usbstack/usb_storage.c b/firmware/usbstack/usb_storage.c index 0a9fa1751e..869a2dd3ed 100644 --- a/firmware/usbstack/usb_storage.c +++ b/firmware/usbstack/usb_storage.c @@ -351,6 +351,7 @@ static unsigned char* ramdisk_buffer; static enum { WAITING_FOR_COMMAND, + WAITING_FOR_STORAGE, SENDING_BLOCKS, SENDING_RESULT, SENDING_FAILED_RESULT, @@ -520,6 +521,9 @@ static void usb_storage_transfer_complete(int ep,int dir,int status,int length) account_data_transfer(dir, status, length); switch(state) { + case WAITING_FOR_STORAGE: + /* The CBW is retained until usb_storage_notify_event(). */ + break; case RECEIVING_BLOCKS: if(dir==USB_DIR_IN) { logf("IN received in RECEIVING"); @@ -734,6 +738,11 @@ static bool usb_storage_control_request(struct usb_ctrlrequest* req, uint8_t* re case USB_BULK_RESET_REQUEST: logf("ums: bulk reset"); + if(state == WAITING_FOR_STORAGE) { + /* Discard the retained CBW on host reset and accept a new + * one; do not replay the cancelled command when ready. */ + usb_drv_recv_nonblocking(EP_OUT, cbw_buffer, MAX_CBW_SIZE); + } state = WAITING_FOR_COMMAND; /* UMS BOT 3.1 says The device shall preserve the value of its bulk data toggle bits and endpoint STALL conditions despite @@ -786,7 +795,7 @@ static void send_and_read_next(void) } /****************************************************************************/ -static void handle_scsi(struct command_block_wrapper* cbw) +static void handle_scsi_ready(struct command_block_wrapper* cbw) { struct storage_info info; unsigned int length = cbw->data_transfer_length; @@ -841,13 +850,6 @@ static void handle_scsi(struct command_block_wrapper* cbw) switch (cbw->command_block[0]) { case SCSI_TEST_UNIT_READY: logf("scsi test_unit_ready %d",lun); - if(!usb_exclusive_storage()) { - send_csw(UMS_STATUS_FAIL); - cur_sense_data.sense_key=SENSE_NOT_READY; - cur_sense_data.asc=ASC_MEDIUM_NOT_PRESENT; - cur_sense_data.ascq=0; - break; - } if(lun_present) { send_csw(UMS_STATUS_GOOD); } @@ -1424,6 +1426,27 @@ static void handle_scsi(struct command_block_wrapper* cbw) } } +/* SET_CONFIGURATION now precedes the storage handover. Keep the first + * CBW pending until all Rockbox clients have acknowledged/unmounted. This + * avoids falsely reporting an absent disk to a fast host and, importantly, + * prevents READ/WRITE commands from racing Rockbox filesystem users. The + * OUT endpoint is not rearmed until this CBW has received its CSW. */ +static void handle_scsi(struct command_block_wrapper* cbw) +{ + if(!usb_exclusive_storage()) { + state = WAITING_FOR_STORAGE; + return; + } + handle_scsi_ready(cbw); +} + +static void usb_storage_notify_event(intptr_t data) +{ + (void)data; + if(state == WAITING_FOR_STORAGE && usb_exclusive_storage()) + handle_scsi((struct command_block_wrapper*)cbw_buffer); +} + /* Count data only, never the CBW or CSW. A short result must report the * untransferred bytes instead of claiming that the whole request succeeded. */ static void account_data_transfer(int dir, int status, int length) @@ -1554,6 +1577,7 @@ struct usb_class_driver usb_cdrv_storage = { .disconnect = usb_storage_disconnect, .transfer_complete = usb_storage_transfer_complete, .control_request = usb_storage_control_request, + .notify_event = usb_storage_notify_event, #ifdef HAVE_HOTSWAP .notify_hotswap = usb_storage_notify_hotswap, #endif From 3bd18f5a443bf9560c188a189c760c64ae6de1a5 Mon Sep 17 00:00:00 2001 From: Paul Sauro Date: Sun, 20 Sep 2026 15:22:15 +0200 Subject: [PATCH 231/408] usb iap: correct sample rate descriptors and packet cadence Advertise only the implemented 32, 44.1 and 48 kHz stereo formats. Add 128-byte packets at 32 kHz and wrap the 44.1 kHz cadence at ten packets; a uint8_t wrap at 256 otherwise emits extra samples every cycle. Use the serialized UAC header length (including baInterfaceNr), maintain the active alternate setting, accept alt 0 on non-streaming interfaces, and retain the last valid sample rate when a SET_CUR request is rejected. Change-Id: I41dab55fc0c7d2e6474492f97062e9838bc3aebf --- firmware/usbstack/iap/audio.c | 8 ++++++-- firmware/usbstack/usb_iap.c | 25 +++++++++++++------------ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/firmware/usbstack/iap/audio.c b/firmware/usbstack/iap/audio.c index f4706b56d6..32cd196c82 100644 --- a/firmware/usbstack/iap/audio.c +++ b/firmware/usbstack/iap/audio.c @@ -33,6 +33,7 @@ static const unsigned long samprs[] = { SAMPR_48, SAMPR_44, + SAMPR_32, }; struct StagingBuffer { @@ -83,6 +84,9 @@ static size_t calc_packet_size(uint8_t cur_sampr, uint8_t packet_count) { if(cur_sampr == 0) { /*48k*/ return 192; + } else if(cur_sampr == 2) { + /*32k*/ + return 128; } else { /*44.1k*/ return packet_count % 10 == 0 ? 180 : 176; @@ -100,7 +104,7 @@ start: if(exhausted || cur_freq != set_freq) { *ptr = zero_buffer.buf.ptr; *len = packet_size; - packet_count += 1; + packet_count = (packet_count + 1) % 10; return; } @@ -134,7 +138,7 @@ start: *len = stage->cursor; stage->cursor = 0; staging_buffer_index = (staging_buffer_index + 1) % USB_BATCH_SLOTS; - packet_count += 1; + packet_count = (packet_count + 1) % 10; #if AUDIO_STAT == 1 sample += packet_size / 4; if(current_tick >= last_hz + HZ) { diff --git a/firmware/usbstack/usb_iap.c b/firmware/usbstack/usb_iap.c index 6b2cfd6fe9..3c63527744 100644 --- a/firmware/usbstack/usb_iap.c +++ b/firmware/usbstack/usb_iap.c @@ -119,23 +119,16 @@ static struct usb_as_interface ipod_audio_stream_1_uac_header = { .wFormatTag = USB_AS_FORMAT_TYPE_I_PCM, }; -/* TODO: remove unsupported freqs */ static struct usb_as_format_type_i_discrete ipod_audio_stream_1_uac_discrete = { - .bLength = USB_AS_SIZEOF_FORMAT_TYPE_I_DISCRETE(9), + .bLength = USB_AS_SIZEOF_FORMAT_TYPE_I_DISCRETE(3), .bDescriptorType = USB_DT_CS_INTERFACE, .bDescriptorSubType = USB_AS_FORMAT_TYPE, .bFormatType = USB_AS_FORMAT_TYPE_I, .bNrChannels = 2, .bSubframeSize = 2, /* bBitResolution / 8 */ .bBitResolution = 16, - .bSamFreqType = 9, + .bSamFreqType = 3, .tSamFreq = { - {0x40, 0x1F, 0x00}, /* 8000 */ - {0x11, 0x2B, 0x00}, /* 11025 */ - {0xE0, 0x2E, 0x00}, /* 12000 */ - {0x80, 0x3E, 0x00}, /* 16000 */ - {0x22, 0x56, 0x00}, /* 22050 */ - {0xC0, 0x5D, 0x00}, /* 24000 */ {0x00, 0x7D, 0x00}, /* 32000 */ {0x44, 0xAC, 0x00}, /* 44100 */ {0x80, 0xBB, 0x00}, /* 48000 */ @@ -339,7 +332,8 @@ static int usb_iap_get_config_descriptor(unsigned char* dest, int max_packet_siz PACK_DESC(ipod_audio_control_desc); ipod_audio_control_uac_header.baInterfaceNr[0] = stream.interface; ipod_audio_control_uac_header.wTotalLength = - sizeof(ipod_audio_control_uac_header) + + /* sizeof omits the flexible baInterfaceNr array. */ + ipod_audio_control_uac_header.bLength + sizeof(ipod_audio_control_uac_input_terminal) + sizeof(ipod_audio_control_uac_output_terminal); PACK_DESC(ipod_audio_control_uac_header); @@ -375,6 +369,7 @@ static int usb_iap_get_config_descriptor(unsigned char* dest, int max_packet_siz static int usb_iap_init_connection(void) { stream.sample_rate = 48000; + stream.alt = 0; last_charge_state = -1; last_minute = -1; last_hold_switch_state = -1; @@ -424,6 +419,8 @@ cleanup_audio: static int usb_iap_set_interface(int intf, int alt) { LOG("set interface interface=%d alt=%d", intf, alt); + if(intf == ctrl.interface || intf == hid.interface) + return alt == 0 ? 0 : -1; check_act(intf == stream.interface, return -1); if(alt == 0) { check_act(iap_audio_disable(), return -1); @@ -433,11 +430,14 @@ static int usb_iap_set_interface(int intf, int alt) { ERROR("invalid alt %d", alt); return -1; } + stream.alt = alt; return 0; } static int usb_iap_get_interface(int intf) { LOG("get interface interface=%d", intf); + if(intf == ctrl.interface || intf == hid.interface) + return 0; check_act(intf == stream.interface, return -1); return stream.alt; } @@ -572,9 +572,10 @@ static bool control_request_if_endpoint(struct usb_ctrlrequest* req, uint8_t* re switch(control_selector) { case USB_AS_EP_CS_SAMPLING_FREQ_CTL: check_act(req->wLength == 3, goto stall); - stream.sample_rate = reqdata[0] | (reqdata[1] << 8) | (reqdata[2] << 16); + uint32_t sample_rate = reqdata[0] | (reqdata[1] << 8) | (reqdata[2] << 16); + check_act(iap_audio_set_sampr(sample_rate), goto stall); + stream.sample_rate = sample_rate; LOG("audio stream sampling rate %lu", stream.sample_rate); - check_act(iap_audio_set_sampr(stream.sample_rate), goto stall); break; } usb_core_control_response(USB_CONTROL_ACK, NULL, 0); From bd615cf125939c760976e22cbee96c7293c69f35 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Wed, 23 Sep 2026 16:54:55 -0400 Subject: [PATCH 232/408] FS#13996: Have audio_hard_stop() kill the PCM output path entirely We already shut down the playback path and the voice path, but that doesn't necessarily mean the PCM hardware is idle. Add a call to pcm_play_stop() to ensure the PCM sink is completely idle. This will prevent a badly-timed callback from firing during a ROLO operation. Change-Id: I1eb8c105895fb47bc3d0af91b4d87345f5399aa4 --- apps/playback.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/playback.c b/apps/playback.c index 29f06dcb69..abb50345f7 100644 --- a/apps/playback.c +++ b/apps/playback.c @@ -27,6 +27,7 @@ #include "core_alloc.h" #include "sound.h" #include "pcm_sink.h" +#include "pcm-internal.h" #include "codecs.h" #include "codec_thread.h" #include "voice_thread.h" @@ -3996,8 +3997,11 @@ void audio_hard_stop(void) LOGFQUEUE("audio >| audio Q_AUDIO_STOP: 1"); audio_queue_send(Q_AUDIO_STOP, 1); #ifdef PLAYBACK_VOICE + /* Stop voice */ voice_stop(); #endif + /* Stop PCM */ + pcm_play_stop(); audiobuf_handle = core_free(audiobuf_handle); } From 841007dfa17c8979189bfe224e1fbb698c4d0353 Mon Sep 17 00:00:00 2001 From: Paul Sauro Date: Sun, 20 Sep 2026 17:21:42 +0200 Subject: [PATCH 233/408] usb: let controller drivers handle SET_ADDRESS requests Handle SET_ADDRESS directly when each controller receives SETUP, before passing other requests to the core. Cancel transfers, send the status response in the driver and queue usb_core_notify_set_address(); the core only updates its address/configuration state. Remove usb_drv_set_address and avoid a request callback from the core back into the controller. Keep status completions for this driver-owned request out of the core EP0 state machine. ARC writes DEVICEADDR and notifies after successful status IN completion; other controllers retain their existing register/status ordering or automatic address handling. Use IRQ-local operations on DesignWare rather than helpers which unconditionally re-enable its IRQ. Take the low seven bits of wValue; do not impose new validation on the unspecified wIndex/wLength fields. Convert all eleven firmware backends; the separate hwstub API is unchanged. Change-Id: I7b96275df90c14ef4a9954f04ac6fdc004ec8d6c --- firmware/drivers/isp1583.c | 43 ++++++++--- firmware/drivers/m66591.c | 34 +++++++-- firmware/drivers/usb-designware.c | 61 +++++++++------ firmware/export/usb_core.h | 3 +- firmware/export/usb_drv.h | 7 +- firmware/target/arm/as3525/usb-drv-as3525.c | 48 +++++++++--- firmware/target/arm/rk27xx/usb-drv-rk27xx.c | 44 +++++++---- .../sansa-connect/tnetv105_usb_drv.c | 74 +++++++++++-------- firmware/target/arm/usb-drv-arc.c | 46 ++++++++---- firmware/target/arm/usb-s3c6400x.c | 46 ++++++++---- firmware/target/arm/usb-tcc.c | 45 ++++++++--- .../target/mips/ingenic_jz47xx/usb-jz4740.c | 47 ++++++++---- .../target/mips/ingenic_jz47xx/usb-jz4760.c | 67 +++++++++++------ firmware/usbstack/usb_core.c | 16 +--- 14 files changed, 392 insertions(+), 189 deletions(-) diff --git a/firmware/drivers/isp1583.c b/firmware/drivers/isp1583.c index 5392b56b6e..eced3dd63d 100644 --- a/firmware/drivers/isp1583.c +++ b/firmware/drivers/isp1583.c @@ -66,6 +66,17 @@ static struct usb_endpoint endpoints[USB_NUM_ENDPOINTS]; static bool high_speed_mode = false; +static bool address_status; +static bool handle_set_address(struct usb_ctrlrequest *req); + +/* SET_ADDRESS status belongs to this driver, not the core EP0 machine. */ +static void complete_transfer(int ep, int dir, int status, int length) +{ + if(ep == EP_CONTROL && address_status) + return; + usb_core_transfer_complete(ep, dir, status, length); +} + static inline void or_int_value(volatile unsigned short *a, volatile unsigned short *b, unsigned long r, unsigned long value) { set_int_value(*a, *b, (r | value)); @@ -342,7 +353,8 @@ static void usb_handle_setup_rx(void) if (len == 8) { ISP1583_DFLOW_CTRLFUN |= DFLOW_CTRLFUN_STATUS; /* Acknowledge packet */ - usb_core_setup_received((struct usb_ctrlrequest*)setup_pkt_buf); + if(!handle_set_address((struct usb_ctrlrequest*)setup_pkt_buf)) + usb_core_setup_received((struct usb_ctrlrequest*)setup_pkt_buf); } else { @@ -529,7 +541,7 @@ static void out_callback(int ep, unsigned char *buf, int len) (void)buf; logf("out_callback(%d, 0x%x, %d)", ep, (int)buf, len); usb_status_ack(ep, DIR_RX); - usb_core_transfer_complete(ep, true, 0, len); /* 0=>status succeeded, haven't worked out status failed yet... */ + complete_transfer(ep, true, 0, len); /* 0=>status succeeded, haven't worked out status failed yet... */ } static void in_callback(int ep, unsigned char *buf, int len) @@ -537,7 +549,7 @@ static void in_callback(int ep, unsigned char *buf, int len) (void)buf; logf("in_callback(%d, 0x%x, %d)", ep, (int)buf, len); usb_status_ack(ep, DIR_TX); - usb_core_transfer_complete(ep, false, 0, len); + complete_transfer(ep, false, 0, len); } int usb_drv_recv_nonblocking(int ep, void* ptr, int length) @@ -679,6 +691,7 @@ void IRAM_ATTR usb_drv_int(void) logf("BRESET"); high_speed_mode = false; bus_reset(); + address_status = false; usb_core_bus_reset(); /* Mask bus reset interrupt */ set_int_value(ISP1583_GEN_INT_A, ISP1583_GEN_INT_B, INT_IEBRST); @@ -736,14 +749,6 @@ void IRAM_ATTR usb_drv_int(void) ZVM_SPECIFIC; } -void usb_drv_set_address(int address) -{ - logf("usb_drv_set_address(0x%x)", address); - ISP1583_INIT_ADDRESS = (address & 0x7F) | INIT_ADDRESS_DEVEN; - - ZVM_SPECIFIC; -} - void usb_drv_set_test_mode(int mode) { logf("usb_drv_set_test_mode(%d)", mode); @@ -819,3 +824,19 @@ const char* dbg_usb_item(int selected_item, void *data, (void)data; } #endif + +static bool handle_set_address(struct usb_ctrlrequest *req) +{ + address_status = false; + if(!usb_drv_is_set_address(req)) + return false; + + const uint8_t address = req->wValue & 0x7f; + address_status = true; + usb_drv_cancel_all_transfers(); + usb_drv_send_nonblocking(EP_CONTROL, NULL, 0); + ISP1583_INIT_ADDRESS = address | INIT_ADDRESS_DEVEN; + ZVM_SPECIFIC; + usb_core_notify_set_address(address); + return true; +} diff --git a/firmware/drivers/m66591.c b/firmware/drivers/m66591.c index 7a4f7da1c8..b891e17e0f 100644 --- a/firmware/drivers/m66591.c +++ b/firmware/drivers/m66591.c @@ -93,6 +93,17 @@ static volatile unsigned short * pipe_ctrl_addr(int pipe) { } } +static bool address_status; +static bool handle_set_address(struct usb_ctrlrequest *req); + +/* SET_ADDRESS status belongs to this driver, not the core EP0 machine. */ +static void complete_transfer(int ep, int dir, int status, int length) +{ + if(ep == EP_CONTROL && address_status) + return; + usb_core_transfer_complete(ep, dir, status, length); +} + static void pipe_init(int pipe) { volatile unsigned short *pipe_cfg; pipe_cfg = pipe_ctrl_addr(pipe); @@ -209,7 +220,8 @@ static void control_received(void) { /* acknowledge packet recieved (clear valid) */ M66591_INTSTAT_MAIN &= ~(1<<3); - usb_core_setup_received(&temp); + if(!handle_set_address(&temp)) + usb_core_setup_received(&temp); } /* This is a helper function, it is used to notife the stack that a transfer is @@ -219,7 +231,7 @@ static void transfer_complete(int endpoint) { M66591_INTCFG_EMP &= ~(1 << endpoint); logf("mxx: ep %d transfer complete", endpoint); int temp=M66591_eps[endpoint].dir ? USB_DIR_IN : USB_DIR_OUT; - usb_core_transfer_complete(endpoint, temp, 0, + complete_transfer(endpoint, temp, 0, M66591_eps[endpoint].count); } @@ -626,9 +638,7 @@ void USB_DEVICE(void) { ******************************************************************************/ /* The M55691 handles this automatically, nothing to do */ -void usb_drv_set_address(int address) { - (void) address; -} + /* This function sets the standard test modes, it is not required, but might as * well implement it since the hardware supports it @@ -935,3 +945,17 @@ void usb_drv_cancel_all_transfers(void) restore_irq(flags); } + +static bool handle_set_address(struct usb_ctrlrequest *req) +{ + address_status = false; + if(!usb_drv_is_set_address(req)) + return false; + + const uint8_t address = req->wValue & 0x7f; + address_status = true; + usb_drv_cancel_all_transfers(); + usb_drv_send_nonblocking(EP_CONTROL, NULL, 0); + usb_core_notify_set_address(address); + return true; +} diff --git a/firmware/drivers/usb-designware.c b/firmware/drivers/usb-designware.c index b6f1691176..8440eafb1e 100644 --- a/firmware/drivers/usb-designware.c +++ b/firmware/drivers/usb-designware.c @@ -147,6 +147,17 @@ static uint32_t epmis_msk; static uint32_t ep_periodic_msk; #endif +static bool address_status; +static bool handle_set_address(struct usb_ctrlrequest *req); + +/* SET_ADDRESS status belongs to this driver, not the core EP0 machine. */ +static void complete_transfer(int ep, int dir, int status, int length) +{ + if(ep == EP_CONTROL && address_status) + return; + usb_core_transfer_complete(ep, dir, status, length); +} + static struct usb_dw_ep *usb_dw_get_ep(int epnum, enum usb_dw_epdir epdir) { return &usb_dw_ep_list[epnum][epdir]; @@ -794,7 +805,7 @@ static void usb_dw_abort_endpoint(int epnum, enum usb_dw_epdir epdir) if (dw_ep->busy) { usb_dw_flush_endpoint(epnum, epdir); - usb_core_transfer_complete(epnum, (epdir == USB_DW_EPDIR_OUT) ? + complete_transfer(epnum, (epdir == USB_DW_EPDIR_OUT) ? USB_DIR_OUT : USB_DIR_IN, -1, 0); } } @@ -879,7 +890,7 @@ static void usb_dw_handle_xfer_complete(int epnum, enum usb_dw_epdir epdir) semaphore_release(&dw_ep->complete); int total_bytes = dw_ep->req_size - dw_ep->sizeleft; - usb_core_transfer_complete(epnum, (epdir == USB_DW_EPDIR_OUT) ? + complete_transfer(epnum, (epdir == USB_DW_EPDIR_OUT) ? USB_DIR_OUT : USB_DIR_IN, dw_ep->status, total_bytes); } @@ -893,12 +904,8 @@ static void usb_dw_handle_setup_received(void) usb_dw_flush_endpoint(0, USB_DW_EPDIR_IN); usb_dw_ep0_recv(); - if ((req.bRequestType & USB_RECIP_MASK) == USB_RECIP_DEVICE && - (req.bRequestType & USB_TYPE_MASK) == USB_TYPE_STANDARD && - (req.bRequest == USB_REQ_SET_ADDRESS)) - usb_dw_set_address(req.wValue); - - usb_core_setup_received(&req); + if(!handle_set_address(&req)) + usb_core_setup_received(&req); } #ifdef USB_DW_SHARED_FIFO @@ -1103,6 +1110,7 @@ static void usb_dw_irq(void) DWC_GINTSTS = USBRST; usb_dw_set_address(0); usb_dw_reset_endpoints(); + address_status = false; usb_core_bus_reset(); } @@ -1362,21 +1370,6 @@ void usb_drv_stall(int endpoint, bool stall, bool in) usb_dw_target_enable_irq(); } -void usb_drv_set_address(int address) -{ -#if 1 - /* Ignored intentionally, because the controller requires us to set the - new address before sending the response for some reason. So we'll - already set it when the control request arrives, before passing that - into the USB core, which will then call this dummy function. */ - (void)address; -#else - usb_dw_target_disable_irq(); - usb_dw_set_address(address); - usb_dw_target_enable_irq(); -#endif -} - int usb_drv_port_speed(void) { return ((DWC_DSTS & 0x6) == 0); @@ -1544,3 +1537,25 @@ int usb_drv_get_frame_number(void) else return (DWC_DSTS >> 8) & 0x3FFF; } + +static bool handle_set_address(struct usb_ctrlrequest *req) +{ + address_status = false; + if(!usb_drv_is_set_address(req)) + return false; + + const uint8_t address = req->wValue & 0x7f; + address_status = true; + /* Already in the IRQ handler: avoid the public IRQ-mask wrappers. */ + for(int ep = 1; ep < USB_NUM_ENDPOINTS; ep++) + for(int dir = 0; dir < USB_DW_NUM_DIRS; dir++) + if((usb_endpoints & (1 << (ep + USB_DW_DIR_OFF(dir)))) && + usb_dw_get_ep(ep, dir)->active) { + usb_dw_abort_endpoint(ep, dir); + DWC_EPCTL(ep, dir) |= SETD0PIDEF; + } + usb_dw_set_address(address); + usb_dw_transfer(0, USB_DW_EPDIR_IN, NULL, 0); + usb_core_notify_set_address(address); + return true; +} diff --git a/firmware/export/usb_core.h b/firmware/export/usb_core.h index e995448fdd..d59c980861 100644 --- a/firmware/export/usb_core.h +++ b/firmware/export/usb_core.h @@ -67,7 +67,8 @@ void usb_core_handle_transfer_completion( #endif void usb_core_handle_notify(long id, intptr_t data); /* For controllers which handle SET ADDR and/or SET CONFIG in hardware */ -void usb_core_notify_set_address(uint8_t addr); +/* Address state only; hardware programming and status timing belong to drivers. */ +void usb_core_notify_set_address(uint8_t addr); /* IRQ-safe notification */ void usb_core_notify_set_config(uint8_t config); #ifdef HAVE_HOTSWAP diff --git a/firmware/export/usb_drv.h b/firmware/export/usb_drv.h index 4d197715e1..3c6b03adad 100644 --- a/firmware/export/usb_drv.h +++ b/firmware/export/usb_drv.h @@ -105,7 +105,12 @@ int usb_drv_send(int endpoint, void* ptr, int length); int usb_drv_send_nonblocking(int endpoint, void* ptr, int length); int usb_drv_recv_blocking(int endpoint, void* ptr, int length); int usb_drv_recv_nonblocking(int endpoint, void* ptr, int length); -void usb_drv_set_address(int address); +/* SET_ADDRESS is owned by the driver, including its status stage. */ +static inline bool usb_drv_is_set_address(const struct usb_ctrlrequest *req) +{ + return req->bRequestType == (USB_TYPE_STANDARD | USB_RECIP_DEVICE) && + req->bRequest == USB_REQ_SET_ADDRESS; +} void usb_drv_reset_endpoint(int endpoint, bool send); bool usb_drv_powered(void); int usb_drv_port_speed(void); diff --git a/firmware/target/arm/as3525/usb-drv-as3525.c b/firmware/target/arm/as3525/usb-drv-as3525.c index ba0b485f1b..88e501ecb1 100644 --- a/firmware/target/arm/as3525/usb-drv-as3525.c +++ b/firmware/target/arm/as3525/usb-drv-as3525.c @@ -58,7 +58,19 @@ static struct usb_dev_dma_desc dmadescs[USB_NUM_EPS][2] __attribute__((aligned(3 /* reuse unused EP2 OUT descriptor here */ static struct usb_dev_setup_buf *setup_desc = (void*)&dmadescs[2][1]; +static bool address_status; +static bool handle_set_address(struct usb_ctrlrequest *req); + +/* SET_ADDRESS status belongs to this driver, not the core EP0 machine. */ +static void complete_transfer(int ep, int dir, int status, int length) +{ + if(ep == EP_CONTROL && address_status) + return; + usb_core_transfer_complete(ep, dir, status, length); +} + #if AS3525_MCLK_SEL != AS3525_CLK_PLLB + static inline void usb_enable_pll(void) { CGU_COUNTB = CGU_LOCK_CNT; @@ -181,7 +193,7 @@ static void reset_endpoints(int init) endpoints[i][0].rc = -1; semaphore_release(&endpoints[i][0].complete); } else { - usb_core_transfer_complete(i, USB_DIR_IN, -1, 0); + complete_transfer(i, USB_DIR_IN, -1, 0); } } endpoints[i][0].state = 0; @@ -189,7 +201,7 @@ static void reset_endpoints(int init) if (i != 2) { /* Skip the OUT EP0 alias */ if (endpoints[i][1].state & EP_STATE_BUSY) - usb_core_transfer_complete(i, USB_DIR_OUT, -1, 0); + complete_transfer(i, USB_DIR_OUT, -1, 0); endpoints[i][1].state = 0; semaphore_wait(&endpoints[i][1].complete, TIMEOUT_NOBLOCK); USB_OEP_SUP_PTR(i) = 0; @@ -555,7 +567,7 @@ static void handle_in_ep(int ep) endpoints[ep][0].uc_desc->status); if (endpoints[ep][0].state & EP_STATE_ASYNC) { endpoints[ep][0].state &= ~EP_STATE_ASYNC; - usb_core_transfer_complete(ep, USB_DIR_IN, 0, endpoints[ep][0].len); + complete_transfer(ep, USB_DIR_IN, 0, endpoints[ep][0].len); } else { semaphore_release(&endpoints[ep][0].complete); } @@ -606,7 +618,7 @@ static void handle_out_ep(int ep) if (endpoints[ep][1].state & EP_STATE_BUSY) { endpoints[ep][1].state &= ~EP_STATE_BUSY; endpoints[ep][1].rc = 0; - usb_core_transfer_complete(ep, USB_DIR_OUT, 0, dma_len); + complete_transfer(ep, USB_DIR_OUT, 0, dma_len); } else { logf("EP%d OUT, but no one was listening?\n", ep); } @@ -628,7 +640,8 @@ static void handle_out_ep(int ep) req->wIndex, req->wLength); - usb_core_setup_received(&req_copy); + if(!handle_set_address(&req_copy)) + usb_core_setup_received(&req_copy); setup_desc_init(setup_desc); ep_sts &= ~USB_EP_STAT_SETUP_RCVD; @@ -733,7 +746,8 @@ void INT_USB_FUNC(void) got_set_configuration = 1; set_config.wValue = USB_DEV_STS & USB_DEV_STS_MASK_CFG; - usb_core_setup_received(&set_config); + if(!handle_set_address(&set_config)) + usb_core_setup_received(&set_config); intr &= ~USB_DEV_INTR_SET_CONFIG; } if (intr & USB_DEV_INTR_EARLY_SUSPEND) {/* idle >3ms detected */ @@ -743,7 +757,8 @@ void INT_USB_FUNC(void) if (intr & USB_DEV_INTR_USB_RESET) {/* usb reset from host? */ logf("usb reset\n"); reset_endpoints(1); - usb_core_bus_reset(); + address_status = false; + usb_core_bus_reset(); intr &= ~USB_DEV_INTR_USB_RESET; } if (intr & USB_DEV_INTR_USB_SUSPEND) {/* suspend req from host? */ @@ -791,10 +806,7 @@ void usb_drv_set_test_mode(int mode) } /* handled internally by controller */ -void usb_drv_set_address(int address) -{ - (void)address; -} + void usb_drv_stall(int ep, bool stall, bool in) { @@ -806,3 +818,17 @@ bool usb_drv_stalled(int ep, bool in) { return USB_EP_CTRL(ep, in) & USB_EP_CTRL_STALL; } + +static bool handle_set_address(struct usb_ctrlrequest *req) +{ + address_status = false; + if(!usb_drv_is_set_address(req)) + return false; + + const uint8_t address = req->wValue & 0x7f; + address_status = true; + usb_drv_cancel_all_transfers(); + usb_drv_send_nonblocking(EP_CONTROL, NULL, 0); + usb_core_notify_set_address(address); + return true; +} diff --git a/firmware/target/arm/rk27xx/usb-drv-rk27xx.c b/firmware/target/arm/rk27xx/usb-drv-rk27xx.c index ee46c514fb..a1dda45ba0 100644 --- a/firmware/target/arm/rk27xx/usb-drv-rk27xx.c +++ b/firmware/target/arm/rk27xx/usb-drv-rk27xx.c @@ -108,6 +108,17 @@ static volatile bool set_configuration = false; #undef ENDPOINT +static bool address_status; +static bool handle_set_address(struct usb_ctrlrequest *req); + +/* SET_ADDRESS status belongs to this driver, not the core EP0 machine. */ +static void complete_transfer(int ep, int dir, int status, int length) +{ + if(ep == EP_CONTROL && address_status) + return; + usb_core_transfer_complete(ep, dir, status, length); +} + static void setup_received(void) { static uint32_t setup_data[2]; @@ -118,7 +129,8 @@ static void setup_received(void) setup_data[1] = SETUP2; /* pass setup data to the upper layer */ - usb_core_setup_received((struct usb_ctrlrequest*)setup_data); + if(!handle_set_address((struct usb_ctrlrequest*)setup_data)) + usb_core_setup_received((struct usb_ctrlrequest*)setup_data); } static int max_pkt_size(struct endpoint_t *endp) @@ -185,7 +197,7 @@ static void in_intr(struct endpoint_t *endp) /* finished ? */ if(endp->cnt <= 0) { - usb_core_transfer_complete(endp->ep_num, endp->dir, 0, endp->len); + complete_transfer(endp->ep_num, endp->dir, 0, endp->len); /* release semaphore for blocking transfer */ if(endp->block) semaphore_release(&endp->complete); @@ -214,7 +226,7 @@ static void out_intr(struct endpoint_t *endp) logf("udc: ack(%d) -> %d/%d", endp->ep_num, xfer_size, endp->cnt); /* finished ? */ if(endp->cnt <= 0 || xfer_size < max_pkt_size(endp)) - usb_core_transfer_complete(endp->ep_num, endp->dir, 0, endp->len); + complete_transfer(endp->ep_num, endp->dir, 0, endp->len); else ep_read(endp); } @@ -306,18 +318,6 @@ void usb_drv_ep_deinit(const struct usb_drv_ep_alloc_ctx* ctx, int ep) EN_INT &= ~(1 << (num + 7)); } -/* Set the address (usually it's in a register). - * There is a problem here: some controller want the address to be set between - * control out and ack and some want to wait for the end of the transaction. - * In the first case, you need to write some code special code when getting - * setup packets and ignore this function (have a look at other drives) - */ -void usb_drv_set_address(int address) -{ - (void)address; - /* UDC seems to set this automaticaly */ -} - static int _usb_drv_send(int endpoint, void *ptr, int length, bool block) { logf("udc: send(%x)", endpoint); @@ -541,3 +541,17 @@ void INT_UDC(void) out_intr(endp); } } + +static bool handle_set_address(struct usb_ctrlrequest *req) +{ + address_status = false; + if(!usb_drv_is_set_address(req)) + return false; + + const uint8_t address = req->wValue & 0x7f; + address_status = true; + usb_drv_cancel_all_transfers(); + usb_drv_send_nonblocking(EP_CONTROL, NULL, 0); + usb_core_notify_set_address(address); + return true; +} diff --git a/firmware/target/arm/tms320dm320/sansa-connect/tnetv105_usb_drv.c b/firmware/target/arm/tms320dm320/sansa-connect/tnetv105_usb_drv.c index 6354062d85..e2874614f9 100644 --- a/firmware/target/arm/tms320dm320/sansa-connect/tnetv105_usb_drv.c +++ b/firmware/target/arm/tms320dm320/sansa-connect/tnetv105_usb_drv.c @@ -37,6 +37,17 @@ #ifdef SANSA_CONNECT #define SDRAM_SIZE 0x04000000 +static bool address_status; +static bool handle_set_address(struct usb_ctrlrequest *req); + +/* SET_ADDRESS status belongs to this driver, not the core EP0 machine. */ +static void complete_transfer(int ep, int dir, int status, int length) +{ + if(ep == EP_CONTROL && address_status) + return; + usb_core_transfer_complete(ep, dir, status, length); +} + static void set_tnetv_reset(bool high) { if (high) @@ -655,7 +666,7 @@ static void tnetv_gadget_req_nuke(int epn, bool in) tnetv_usb_reg_write(TNETV_USB_EPx_IN_CNT(epn), 0x80008000); if (ep->tx_remaining > 0) { - usb_core_transfer_complete(epn, USB_DIR_IN, -1, 0); + complete_transfer(epn, USB_DIR_IN, -1, 0); } ep->tx_buf = NULL; ep->tx_remaining = 0; @@ -674,7 +685,7 @@ static void tnetv_gadget_req_nuke(int epn, bool in) tnetv_usb_reg_write(TNETV_USB_EPx_OUT_CNT(epn), 0x80008000); if (ep->rx_remaining > 0) { - usb_core_transfer_complete(epn, USB_DIR_OUT, -1, 0); + complete_transfer(epn, USB_DIR_OUT, -1, 0); } ep->rx_buf = NULL; ep->rx_remaining = 0; @@ -865,7 +876,7 @@ static void in_interrupt(int epn) if (ep->tx_remaining <= 0) { - usb_core_transfer_complete(epn, USB_DIR_IN, 0, ep->tx_size); + complete_transfer(epn, USB_DIR_IN, 0, ep->tx_size); /* release semaphore for blocking transfer */ if (ep->block) { @@ -925,7 +936,7 @@ static void out_interrupt(int epn) is_short = rcv_len && (rcv_len < ep->max_packet_size); if (is_short || (ep->rx_remaining == 0)) { - usb_core_transfer_complete(epn, USB_DIR_OUT, 0, ep->rx_size - ep->rx_remaining); + complete_transfer(epn, USB_DIR_OUT, 0, ep->rx_size - ep->rx_remaining); ep->rx_remaining = 0; ep->rx_size = 0; ep->rx_buf = 0; @@ -955,7 +966,7 @@ static void out_interrupt(int epn) */ if ((ret == 0) && (ep->rx_remaining != ep->rx_size)) { - usb_core_transfer_complete(epn, USB_DIR_OUT, 0, ep->rx_size - ep->rx_remaining); + complete_transfer(epn, USB_DIR_OUT, 0, ep->rx_size - ep->rx_remaining); ep->rx_remaining = 0; ep->rx_size = 0; ep->rx_buf = 0; @@ -1072,7 +1083,8 @@ void VLYNQ(void) tnetv_usb_reg_write(TNETV_USB_STATUS, sysIntClear.val); tnetv_udc_handle_reset(); - usb_core_bus_reset(); + address_status = false; + usb_core_bus_reset(); } if (sysIntrStatus.f.suspend) @@ -1161,18 +1173,9 @@ void VLYNQ(void) sysIntClear.f.setup = 1; tnetv_usb_reg_write(TNETV_USB_STATUS, sysIntClear.val); - if (((setup.bRequestType & USB_RECIP_MASK) == USB_RECIP_DEVICE) && - (setup.bRequest == USB_REQ_SET_ADDRESS)) + if (usb_drv_is_set_address(&setup)) { - /* Rockbox USB core works according to USB specification, i.e. - * it first acknowledges the control transfer and then sets - * the address. However, Linux TNETV105 driver first sets the - * address and then acknowledges the transfer. At first, - * it seemed that Linux driver was wrong, but it seems that - * TNETV105 simply requires such order. It might be documented - * in the datasheet and thus there is no comment in the Linux - * driver about this. - */ + /* Defer endpoint cancellation until the driver handles the address. */ setup_is_set_address = true; } else @@ -1181,7 +1184,8 @@ void VLYNQ(void) } /* Process control packet */ - usb_core_setup_received(&setup); + if(!handle_set_address(&setup)) + usb_core_setup_received(&setup); } if (sysIntrStatus.f.ep0_in_ack) @@ -1363,7 +1367,7 @@ int usb_drv_send(int endpoint, void* ptr, int length) { if (setup_is_set_address) { - /* usb_drv_set_address() will call us later */ + /* The driver control-request handler will send the status. */ return 0; } /* HACK: Do not wait for status stage ZLP @@ -1397,19 +1401,6 @@ int usb_drv_recv_nonblocking(int endpoint, void* ptr, int length) return 0; } -void usb_drv_set_address(int address) -{ - UsbCtrlType usbCtrl; - usbCtrl.val = tnetv_usb_reg_read(TNETV_USB_CTRL); - usbCtrl.f.func_addr = address; - tnetv_usb_reg_write(TNETV_USB_CTRL, usbCtrl.val); - - /* This seems to be the only working order */ - setup_is_set_address = false; - usb_drv_send(EP_CONTROL, NULL, 0); - usb_drv_cancel_all_transfers(); -} - /* return port speed FS=0, HS=1 */ int usb_drv_port_speed(void) { @@ -1506,3 +1497,22 @@ void usb_drv_ep_deinit(const struct usb_drv_ep_alloc_ctx* ctx, int ep) int dir = EP_DIR(ep); tnetv_gadget_ep_disable(num, dir == DIR_IN); } + +static bool handle_set_address(struct usb_ctrlrequest *req) +{ + address_status = false; + if(!usb_drv_is_set_address(req)) + return false; + + const uint8_t address = req->wValue & 0x7f; + address_status = true; + UsbCtrlType usbCtrl; + usbCtrl.val = tnetv_usb_reg_read(TNETV_USB_CTRL); + usbCtrl.f.func_addr = address; + tnetv_usb_reg_write(TNETV_USB_CTRL, usbCtrl.val); + setup_is_set_address = false; + usb_drv_cancel_all_transfers(); + usb_drv_send_nonblocking(EP_CONTROL, NULL, 0); + usb_core_notify_set_address(address); + return true; +} diff --git a/firmware/target/arm/usb-drv-arc.c b/firmware/target/arm/usb-drv-arc.c index 7f3c7758fe..506b656400 100644 --- a/firmware/target/arm/usb-drv-arc.c +++ b/firmware/target/arm/usb-drv-arc.c @@ -370,6 +370,17 @@ static void prepare_td(struct transfer_descriptor* td, static void bus_reset(void); static void init_control_queue_heads(void); /*-------------------------------------------------------------------------*/ +static bool address_status; +static bool handle_set_address(struct usb_ctrlrequest *req); + +/* SET_ADDRESS status belongs to this driver, not the core EP0 machine. */ +static void complete_transfer(int ep, int dir, int status, int length) +{ + if(ep == EP_CONTROL && address_status) + return; + usb_core_transfer_complete(ep, dir, status, length); +} + static void usb_drv_stop(void) { /* disable interrupts */ @@ -558,6 +569,7 @@ void usb_drv_int(void) if (status & USBSTS_RESET) { REG_USBSTS = status; bus_reset(); + address_status = false; usb_core_bus_reset(); return; } @@ -683,13 +695,6 @@ bool usb_drv_powered(void) return (REG_OTGSC & OTGSC_A_VBUS_VALID) ? true : false; } -void usb_drv_set_address(int address) -{ - /* SET_ADDRESS is captured when the setup packet arrives and applied - * after the EP0 IN status stage completes. */ - (void)address; -} - void usb_drv_reset_endpoint(int endpoint, bool send) { int pipe = EP_NUM(endpoint) * 2 + (send ? 1 : 0); @@ -1102,12 +1107,8 @@ static void setup_received(void) /* A new setup packet supersedes any unfinished SET_ADDRESS request. */ pending_device_address = -1; - if ((req->bRequestType & (USB_TYPE_MASK | USB_RECIP_MASK)) == - (USB_TYPE_STANDARD | USB_RECIP_DEVICE) && - req->bRequest == USB_REQ_SET_ADDRESS) - pending_device_address = req->wValue; - - usb_core_setup_received(req); + if(!handle_set_address(req)) + usb_core_setup_received(req); } static void transfer_completed(void) @@ -1145,6 +1146,7 @@ static void transfer_completed(void) qh->status == 0 && length == 0) { REG_DEVICEADDR = pending_device_address << USBDEVICEADDRESS_BIT_POS; + usb_core_notify_set_address(pending_device_address); pending_device_address = -1; } @@ -1153,7 +1155,7 @@ static void transfer_completed(void) semaphore_release(&transfer_completion_signal[pipe]); } - usb_core_transfer_complete(ep, dir?USB_DIR_IN:USB_DIR_OUT, + complete_transfer(ep, dir?USB_DIR_IN:USB_DIR_OUT, qh->status, length); Lskip: continue; @@ -1209,3 +1211,19 @@ static void init_control_queue_heads(void) qh_array[EP_CONTROL+1].max_pkt_length = 64 << QH_MAX_PKT_LEN_POS | QH_ZLT_SEL; qh_array[EP_CONTROL+1].dtd.next_td_ptr = QH_NEXT_TERMINATE; } + +static bool handle_set_address(struct usb_ctrlrequest *req) +{ + address_status = false; + if(!usb_drv_is_set_address(req)) + return false; + + const uint8_t address = req->wValue & 0x7f; + address_status = true; + /* Capture before sending; the completion IRQ writes DEVICEADDR and + * notifies the core after the successful zero-length status packet. */ + pending_device_address = address; + usb_drv_cancel_all_transfers(); + usb_drv_send_nonblocking(EP_CONTROL, NULL, 0); + return true; +} diff --git a/firmware/target/arm/usb-s3c6400x.c b/firmware/target/arm/usb-s3c6400x.c index 79191eec35..99d84f63c1 100644 --- a/firmware/target/arm/usb-s3c6400x.c +++ b/firmware/target/arm/usb-s3c6400x.c @@ -38,9 +38,21 @@ //#define LOGF_ENABLE #include "logf.h" +static bool address_status; +static bool handle_set_address(struct usb_ctrlrequest *req); + +/* SET_ADDRESS status belongs to this driver, not the core EP0 machine. */ +static void complete_transfer(int ep, int dir, int status, int length) +{ + if(ep == EP_CONTROL && address_status) + return; + usb_core_transfer_complete(ep, dir, status, length); +} + #if CONFIG_CPU == AS3525v2 #define UNCACHED_ADDR AS3525_UNCACHED_ADDR #define PHYSICAL_ADDR AS3525_PHYSICAL_ADDR + static inline void discard_dma_buffer_cache(void) {} #elif CONFIG_CPU == S5L8702 #define UNCACHED_ADDR S5L8702_UNCACHED_ADDR @@ -125,15 +137,6 @@ void usb_drv_stall(int endpoint, bool stall, bool in) DEPCTL(endpoint, !in) &= ~DEPCTL_stall; } -void usb_drv_set_address(int address) -{ - (void)address; - /* Ignored intentionally, because the controller requires us to set the - new address before sending the response for some reason. So we'll - already set it when the control request arrives, before passing that - into the USB core, which will then call this dummy function. */ -} - static void ep_transfer(int ep, void *ptr, int len, bool out) { /* disable interrupts to avoid any race */ @@ -489,7 +492,7 @@ static void handle_ep_int(int ep, bool out) } if (!out) endpoint->size = size; - usb_core_transfer_complete(ep, out ? USB_DIR_OUT : USB_DIR_IN, 0, transfered); + complete_transfer(ep, out ? USB_DIR_OUT : USB_DIR_IN, 0, transfered); endpoint->done = true; semaphore_release(&endpoint->complete); } @@ -525,11 +528,11 @@ static void handle_ep_int(int ep, bool out) logf(" rt=%x r=%x", ep0_setup_pkt->bRequestType, ep0_setup_pkt->bRequest); - if(ep0_setup_pkt->bRequestType == USB_TYPE_STANDARD && - ep0_setup_pkt->bRequest == USB_REQ_SET_ADDRESS) - DCFG = (DCFG & ~bitm(DCFG, devadr)) | (ep0_setup_pkt->wValue << DCFG_devadr_bitp); + if(usb_drv_is_set_address(ep0_setup_pkt)) + DCFG = (DCFG & ~bitm(DCFG, devadr)) | ((ep0_setup_pkt->wValue & 0x7f) << DCFG_devadr_bitp); - usb_core_setup_received(ep0_setup_pkt); + if(!handle_set_address(ep0_setup_pkt)) + usb_core_setup_received(ep0_setup_pkt); } } @@ -547,6 +550,7 @@ void INT_USB_FUNC(void) { DCFG &= ~bitm(DCFG, devadr); /* Address 0 */ reset_endpoints(); + address_status = false; usb_core_bus_reset(); } @@ -622,3 +626,17 @@ int usb_drv_send(int ep, void *ptr, int len) semaphore_wait(&endpoint->complete, HZ); return endpoint->status; } + +static bool handle_set_address(struct usb_ctrlrequest *req) +{ + address_status = false; + if(!usb_drv_is_set_address(req)) + return false; + + const uint8_t address = req->wValue & 0x7f; + address_status = true; + usb_drv_cancel_all_transfers(); + usb_drv_send_nonblocking(EP_CONTROL, NULL, 0); + usb_core_notify_set_address(address); + return true; +} diff --git a/firmware/target/arm/usb-tcc.c b/firmware/target/arm/usb-tcc.c index 694b38f225..087ff60412 100644 --- a/firmware/target/arm/usb-tcc.c +++ b/firmware/target/arm/usb-tcc.c @@ -99,6 +99,17 @@ static struct tcc_ep tcc_endpoints[] = { static bool usb_drv_write_ep(struct tcc_ep *ep); static void usb_set_speed(int); +static bool address_status; +static bool handle_set_address(struct usb_ctrlrequest *req); + +/* SET_ADDRESS status belongs to this driver, not the core EP0 machine. */ +static void complete_transfer(int ep, int dir, int status, int length) +{ + if(ep == EP_CONTROL && address_status) + return; + usb_core_transfer_complete(ep, dir, status, length); +} + void usb_drv_ep_init(const struct usb_drv_ep_alloc_ctx* ctx, int ep) { /* FIXME: support max packet size override */ @@ -228,7 +239,8 @@ void handle_control(void) DEBUG(2, "req: %02x %02d", req->bRequestType, req->bRequest); } - usb_core_setup_received(req); + if(!handle_set_address(req)) + usb_core_setup_received(req); } static @@ -280,7 +292,7 @@ void handle_ep_in(struct tcc_ep *tcc_ep, uint16_t stat) TCC7xx_USB_EP_CTRL |= TCC7xx_USB_EP_CTRL_OUTHD; - usb_core_transfer_complete(tcc_ep->id, USB_DIR_OUT, 0, count); + complete_transfer(tcc_ep->id, USB_DIR_OUT, 0, count); } static @@ -306,7 +318,7 @@ void handle_ep_out(struct tcc_ep *tcc_ep, uint16_t stat) TCC7xx_USB_EPIE &= ~tcc_ep->mask; global_ep_irq_mask &= ~tcc_ep->mask; -// usb_core_transfer_complete(tcc_ep->id, USB_DIR_IN, 0, tcc_ep->count); +// complete_transfer(tcc_ep->id, USB_DIR_IN, 0, tcc_ep->count); } } @@ -408,7 +420,8 @@ static void usb_reset(void) global_ep_irq_mask = 0x1; TCC7xx_USB_EPIE = global_ep_irq_mask; - usb_core_bus_reset(); + address_status = false; + usb_core_bus_reset(); } /* IRQ handler */ @@ -462,12 +475,6 @@ void USB_DEVICE(void) TCC7xx_USB_INDEX = index_save; } -void usb_drv_set_address(int address) -{ - (void) address; - DEBUG(2, "setting address %d %d", address, TCC7xx_USB_FUNC); -} - int usb_drv_port_speed(void) { return (TCC7xx_USB_SYS_STAT & 0x10) ? 1 : 0; @@ -504,7 +511,7 @@ static bool usb_drv_write_ep(struct tcc_ep *ep) ep->max_len -= count; if (ep->max_len == 0) { - usb_core_transfer_complete(ep->id, USB_DIR_IN, 0, ep->count); + complete_transfer(ep->id, USB_DIR_IN, 0, ep->count); ep->buf = NULL; // return true; } @@ -632,7 +639,7 @@ void usb_drv_cancel_all_transfers(void) flags = disable_irq_save(); for (endpoint = 0; endpoint < 4; endpoint++) { if (tcc_endpoints[endpoint].buf) { -/* usb_core_transfer_complete(tcc_endpoints[endpoint].id, +/* complete_transfer(tcc_endpoints[endpoint].id, tcc_endpoints[endpoint].dir, -1, 0); */ tcc_endpoints[endpoint].buf = NULL; } @@ -755,6 +762,20 @@ void usb_test(void) } } #endif +static bool handle_set_address(struct usb_ctrlrequest *req) +{ + address_status = false; + if(!usb_drv_is_set_address(req)) + return false; + + const uint8_t address = req->wValue & 0x7f; + address_status = true; + usb_drv_cancel_all_transfers(); + usb_drv_send_nonblocking(EP_CONTROL, NULL, 0); + usb_core_notify_set_address(address); + return true; +} + #else void usb_init_device(void) { diff --git a/firmware/target/mips/ingenic_jz47xx/usb-jz4740.c b/firmware/target/mips/ingenic_jz47xx/usb-jz4740.c index 828dbaf2c4..e87e84fc4b 100644 --- a/firmware/target/mips/ingenic_jz47xx/usb-jz4740.c +++ b/firmware/target/mips/ingenic_jz47xx/usb-jz4740.c @@ -87,6 +87,17 @@ struct usb_drv_ep_spec usb_drv_ep_specs[USB_NUM_ENDPOINTS] = { }; uint8_t usb_drv_ep_specs_flags = 0; +static bool address_status; +static bool handle_set_address(struct usb_ctrlrequest *req); + +/* SET_ADDRESS status belongs to this driver, not the core EP0 machine. */ +static void complete_transfer(int ep, int dir, int status, int length) +{ + if(ep == EP_CONTROL && address_status) + return; + usb_core_transfer_complete(ep, dir, status, length); +} + static inline void select_endpoint(int ep) { REG_USB_REG_INDEX = ep; @@ -199,7 +210,7 @@ static void EP0_send(void) if(ep->sent >= ep->length) { REG_USB_REG_CSR0 = (csr0 | USB_CSR0_INPKTRDY | USB_CSR0_DATAEND); /* Set data end! */ - usb_core_transfer_complete(EP_CONTROL, USB_DIR_IN, 0, ep->sent); + complete_transfer(EP_CONTROL, USB_DIR_IN, 0, ep->sent); ep_transfer_completed(ep); } else @@ -245,7 +256,8 @@ static void EP0_handler(void) { readFIFO(ep_recv, REG_USB_REG_COUNT0); REG_USB_REG_CSR0 = csr0 | USB_CSR0_SVDOUTPKTRDY; /* clear OUTPKTRDY bit */ - usb_core_setup_received((struct usb_ctrlrequest*)ep_recv->buf); + if(!handle_set_address((struct usb_ctrlrequest*)ep_recv->buf)) + usb_core_setup_received((struct usb_ctrlrequest*)ep_recv->buf); } } @@ -292,7 +304,7 @@ static void EPIN_handler(unsigned int endpoint) if(ep->sent >= ep->length) { - usb_core_transfer_complete(endpoint, USB_DIR_IN, 0, ep->sent); + complete_transfer(endpoint, USB_DIR_IN, 0, ep->sent); ep_transfer_completed(ep); logf("sent complete"); } @@ -340,7 +352,7 @@ static void EPOUT_handler(unsigned int endpoint) if(size < ep->fifo_size || ep->received >= ep->length) { - usb_core_transfer_complete(endpoint, USB_DIR_OUT, 0, ep->received); + complete_transfer(endpoint, USB_DIR_OUT, 0, ep->received); ep_transfer_completed(ep); logf("receive transfer_complete"); } @@ -387,7 +399,7 @@ static void EPDMA_handler(int number) REG_USB_REG_INCSR |= USB_INCSR_INPKTRDY; } - usb_core_transfer_complete(endpoint, EP_IS_IN(ep) ? USB_DIR_IN : USB_DIR_OUT, + complete_transfer(endpoint, EP_IS_IN(ep) ? USB_DIR_IN : USB_DIR_OUT, 0, ep->length); ep_transfer_completed(ep); } @@ -486,7 +498,8 @@ static void udc_reset(void) REG_USB_REG_INTRINE |= USB_INTR_EP0; REG_USB_REG_INTRUSBE |= USB_INTR_RESET; - usb_core_bus_reset(); + address_status = false; + usb_core_bus_reset(); } /* Interrupt handler */ @@ -682,13 +695,6 @@ void usb_drv_exit(void) __cpm_stop_udc(); } -void usb_drv_set_address(int address) -{ - logf("%s(%d)", __func__, address); - - REG_USB_REG_FADDR = address; -} - static void usb_drv_send_internal(struct usb_endpoint* ep, void* ptr, int length, bool blocking) { if(ep->type == ep_control && ptr == NULL && length == 0) @@ -847,3 +853,18 @@ void usb_drv_ep_deinit(const struct usb_drv_ep_alloc_ctx* ctx, int ep) (void)ctx; (void)ep; } + +static bool handle_set_address(struct usb_ctrlrequest *req) +{ + address_status = false; + if(!usb_drv_is_set_address(req)) + return false; + + const uint8_t address = req->wValue & 0x7f; + address_status = true; + usb_drv_cancel_all_transfers(); + usb_drv_send_nonblocking(EP_CONTROL, NULL, 0); + REG_USB_REG_FADDR = address; + usb_core_notify_set_address(address); + return true; +} diff --git a/firmware/target/mips/ingenic_jz47xx/usb-jz4760.c b/firmware/target/mips/ingenic_jz47xx/usb-jz4760.c index 217b097a15..17669104c1 100644 --- a/firmware/target/mips/ingenic_jz47xx/usb-jz4760.c +++ b/firmware/target/mips/ingenic_jz47xx/usb-jz4760.c @@ -115,6 +115,17 @@ struct usb_drv_ep_spec usb_drv_ep_specs[USB_NUM_ENDPOINTS] = { }; uint8_t usb_drv_ep_specs_flags = 0; +static bool address_status; +static bool handle_set_address(struct usb_ctrlrequest *req); + +/* SET_ADDRESS status belongs to this driver, not the core EP0 machine. */ +static void complete_transfer(int ep, int dir, int status, int length) +{ + if(ep == EP_CONTROL && address_status) + return; + usb_core_transfer_complete(ep, dir, status, length); +} + static inline void select_endpoint(int ep) { REG_USB_INDEX = ep; @@ -236,7 +247,7 @@ static void EP0_send(void) if (ep->sent >= ep->length) { REG_USB_CSR0 = (csr0 | USB_CSR0_INPKTRDY | USB_CSR0_DATAEND); /* Set data end! */ if (!ep->wait) { - usb_core_transfer_complete(EP_CONTROL, USB_DIR_IN, 0, ep->sent); + complete_transfer(EP_CONTROL, USB_DIR_IN, 0, ep->sent); } ep->rc = 0; ep_transfer_completed(ep); @@ -277,11 +288,11 @@ static void EP0_handler(void) ep0_data_requested = false; if (ep_send->busy) { if (!ep_send->wait) - usb_core_transfer_complete(EP_CONTROL, USB_DIR_IN, -1, 0); + complete_transfer(EP_CONTROL, USB_DIR_IN, -1, 0); ep_transfer_completed(ep_send); } if (ep_recv->busy) { - usb_core_transfer_complete(EP_CONTROL, USB_DIR_OUT, -1, 0); + complete_transfer(EP_CONTROL, USB_DIR_OUT, -1, 0); ep_transfer_completed(ep_recv); } } @@ -290,7 +301,7 @@ static void EP0_handler(void) if (csr0 & USB_CSR0_OUTPKTRDY) { /* There is a packet in the fifo */ if (ep_send->busy) { if (!ep_send->wait) { - usb_core_transfer_complete(EP_CONTROL, USB_DIR_IN, -1, 0); + complete_transfer(EP_CONTROL, USB_DIR_IN, -1, 0); } ep_transfer_completed(ep_send); } @@ -300,7 +311,7 @@ static void EP0_handler(void) ep_recv->received += size; if (size < ep_recv->fifo_size || ep_recv->received >= ep_recv->length) { REG_USB_CSR0 = csr0 | USB_CSR0_SVDOUTPKTRDY | USB_CSR0_DATAEND; /* Set data end! */ - usb_core_transfer_complete(EP_CONTROL, USB_DIR_OUT, 0, ep_recv->received); + complete_transfer(EP_CONTROL, USB_DIR_OUT, 0, ep_recv->received); ep_transfer_completed(ep_recv); } else { REG_USB_CSR0 = csr0 | USB_CSR0_SVDOUTPKTRDY; /* clear OUTPKTRDY bit */ @@ -319,7 +330,8 @@ static void EP0_handler(void) ep0_data_supplied = true; } REG_USB_CSR0 = csr0; - usb_core_setup_received(&ep0_rx.request); + if(!handle_set_address(&ep0_rx.request)) + usb_core_setup_received(&ep0_rx.request); ep_transfer_completed(ep_recv); } } @@ -479,7 +491,7 @@ static void EPIN_complete(unsigned int endpoint) if (ep->sent >= ep->length) { if (!ep->wait) - usb_core_transfer_complete(endpoint, USB_DIR_IN, 0, ep->sent); + complete_transfer(endpoint, USB_DIR_IN, 0, ep->sent); ep->rc = 0; ep_transfer_completed(ep); logf("send complete"); @@ -577,7 +589,7 @@ static void EPOUT_handler(unsigned int endpoint) logf("received: %d max length: %d", ep->received, ep->length); if (size < ep->fifo_size || ep->received >= ep->length) { - usb_core_transfer_complete(endpoint, USB_DIR_OUT, 0, ep->received); + complete_transfer(endpoint, USB_DIR_OUT, 0, ep->received); ep_transfer_completed(ep); logf("receive transfer_complete"); } @@ -633,7 +645,7 @@ static void EPOUT_ready(unsigned int endpoint) /* It we're done, clean up */ if (size < ep->fifo_size || ep->received >= ep->length) { ep->use_dma = -1; - usb_core_transfer_complete(endpoint, USB_DIR_OUT, 0, ep->received); + complete_transfer(endpoint, USB_DIR_OUT, 0, ep->received); ep_transfer_completed(ep); logf("DMA RX transfer_complete"); return; @@ -704,9 +716,9 @@ static void setup_endpoint(struct usb_endpoint *ep) if (ep->wait) semaphore_release(&ep->complete); else - usb_core_transfer_complete(endpoint, USB_DIR_IN, -1, 0); + complete_transfer(endpoint, USB_DIR_IN, -1, 0); } else { - usb_core_transfer_complete(endpoint, USB_DIR_OUT, -1, 0); + complete_transfer(endpoint, USB_DIR_OUT, -1, 0); } } @@ -796,7 +808,7 @@ static void udc_reset(void) if (endpoints[0].wait) semaphore_release(&endpoints[0].complete); else - usb_core_transfer_complete(EP_CONTROL, USB_DIR_IN, -1, 0); + complete_transfer(EP_CONTROL, USB_DIR_IN, -1, 0); } endpoints[0].busy = false; @@ -806,7 +818,7 @@ static void udc_reset(void) endpoints[0].allocated = true; if (endpoints[1].busy) - usb_core_transfer_complete(EP_CONTROL, USB_DIR_OUT, -1, 0); + complete_transfer(EP_CONTROL, USB_DIR_OUT, -1, 0); endpoints[1].busy = false; endpoints[1].wait = false; @@ -825,7 +837,8 @@ static void udc_reset(void) REG_USB_INTRINE |= USB_INTR_EP(0); REG_USB_INTRUSBE |= USB_INTR_RESET; - usb_core_bus_reset(); + address_status = false; + usb_core_bus_reset(); } /* Interrupt handler */ @@ -1018,13 +1031,6 @@ void usb_drv_exit(void) REG_USB_POWER &= ~USB_POWER_SOFTCONN; } -void usb_drv_set_address(int address) -{ - logf("%s(%d)", __func__, address); - - REG_USB_FADDR = address; -} - static void usb_drv_send_internal(struct usb_endpoint* ep, void* ptr, int length, bool blocking) { int flags = disable_irq_save(); @@ -1171,11 +1177,11 @@ void usb_drv_cancel_all_transfers(void) if (endpoints[i].busy) { if (i & 1) - usb_core_transfer_complete(i >> 1, USB_DIR_OUT, -1, 0); + complete_transfer(i >> 1, USB_DIR_OUT, -1, 0); else if (endpoints[i].wait) semaphore_release(&endpoints[i].complete); else - usb_core_transfer_complete(i >> 1, USB_DIR_IN, -1, 0); + complete_transfer(i >> 1, USB_DIR_IN, -1, 0); } if(i != 1) /* ep0 out needs special handling */ @@ -1217,3 +1223,18 @@ void usb_drv_ep_deinit(const struct usb_drv_ep_alloc_ctx* ctx, int ep) else REG_USB_INTROUTE &= ~USB_INTR_EP(num); } + +static bool handle_set_address(struct usb_ctrlrequest *req) +{ + address_status = false; + if(!usb_drv_is_set_address(req)) + return false; + + const uint8_t address = req->wValue & 0x7f; + address_status = true; + usb_drv_cancel_all_transfers(); + usb_drv_send_nonblocking(EP_CONTROL, NULL, 0); + REG_USB_FADDR = address; + usb_core_notify_set_address(address); + return true; +} diff --git a/firmware/usbstack/usb_core.c b/firmware/usbstack/usb_core.c index 69dbf1f3e5..efe123cc41 100644 --- a/firmware/usbstack/usb_core.c +++ b/firmware/usbstack/usb_core.c @@ -862,7 +862,7 @@ static void request_handler_device_get_descriptor(struct usb_ctrlrequest* req, u } } -static void usb_core_do_set_addr(uint8_t address) +static void usb_core_set_address(uint8_t address) { logf("usb_core: SET_ADR %d", address); usb_address = address; @@ -962,8 +962,6 @@ static void usb_core_do_clear_feature(int recip, int recip_nr, int feature) static void request_handler_device(struct usb_ctrlrequest* req, uint8_t* reqdata, size_t reqdata_size) { - unsigned address; - switch(req->bRequest) { case USB_REQ_GET_CONFIGURATION: logf("usb_core: GET_CONFIG"); @@ -978,16 +976,6 @@ static void request_handler_device(struct usb_ctrlrequest* req, uint8_t* reqdata usb_core_control_response(USB_CONTROL_STALL, NULL, 0); } break; - case USB_REQ_SET_ADDRESS: - /* NOTE: We really have no business handling this and drivers - * should just handle it themselves. We don't care beyond - * knowing if we've been assigned an address yet, or not. */ - address = req->wValue; - usb_drv_cancel_all_transfers(); - usb_core_control_response(USB_CONTROL_ACK, NULL, 0); - usb_drv_set_address(address); - usb_core_do_set_addr(address); - break; case USB_REQ_GET_DESCRIPTOR: logf("usb_core: GET_DESC %d", req->wValue >> 8); request_handler_device_get_descriptor(req, reqdata, reqdata_size); @@ -1301,7 +1289,7 @@ void usb_core_handle_notify(long id, intptr_t data) switch(id) { case USB_NOTIFY_SET_ADDR: - usb_core_do_set_addr(data); + usb_core_set_address(data); break; case USB_NOTIFY_SET_CONFIG: usb_core_do_set_config(data); From 7b4d1a7f7537cf95f7429890b3da8207d1d61a64 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:27 -0400 Subject: [PATCH 234/408] opus: dual_inner_prod on ARMv5E One ldr fetches two celt_norm coefficients and smlabb/smlatt take the halves apart; scalar fallback when the three pointers disagree on alignment. Modelled: -0.88% ARMv5E. Measured: Clip+ 28.13 -> 28.08 MHz, the same build with and without it. Co-Authored-By: Claude Opus 5 Change-Id: I79719132ccecfb7664616bca019e3856ee3479da --- lib/rbcodec/codecs/libopus/SOURCES | 1 + .../codecs/libopus/celt/arm/pitch_arm.h | 17 +++ .../libopus/celt/arm/pitch_armv5e_asm.S | 121 ++++++++++++++++++ lib/rbcodec/codecs/libopus/config.h | 1 + 4 files changed, 140 insertions(+) create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/pitch_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index 16f1caed82..89a0428175 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -17,6 +17,7 @@ celt/arm/deemph_armv4_asm.S #elif defined(CPU_ARM) && (ARM_ARCH >= 5) && (ARCH_PROFILE == ARM_PROFILE_CLASSIC) celt/arm/kiss_fft_armv5e_asm.S celt/arm/mdct_armv5e_asm.S +celt/arm/pitch_armv5e_asm.S celt/arm/comb_filter_armv5e_asm.S celt/arm/denorm_armv5e_asm.S celt/arm/exp_rotation1_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/celt/arm/pitch_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/pitch_arm.h index bed8b04eac..e667abb10e 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/pitch_arm.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/pitch_arm.h @@ -66,6 +66,23 @@ extern void (*const DUAL_INNER_PROD_IMPL[OPUS_ARCHMASK+1])(const opus_val16 *x, # endif # endif +/* The decoder reaches dual_inner_prod from one place, stereo_merge. The C + reads three signed halfwords per element and stalls on each of them, since + an LDRSH result lands two cycles late on this core; one LDR fetches two + coefficients instead and smlabb/smlatt take the halves apart with no sign + extension of their own. Measured at 12.1 cycles per element before and + 5.5 after. ARMv4 is deliberately left alone: without the packed + multiplies the extraction would cost exactly what the saved loads buy. + Escape hatch OPUS_ARM_NO_PITCH_ASM. */ +# if defined(OPUS_ARM_INLINE_EDSP) && defined(FIXED_POINT) \ + && !defined(OVERRIDE_DUAL_INNER_PROD) && !defined(OPUS_ARM_NO_PITCH_ASM) +void dual_inner_prod_armv5e(const opus_val16 *x, const opus_val16 *y01, + const opus_val16 *y02, int N, opus_val32 *xy1, opus_val32 *xy2); +# define OVERRIDE_DUAL_INNER_PROD (1) +# define dual_inner_prod(x, y01, y02, N, xy1, xy2, arch) \ + ((void)(arch), dual_inner_prod_armv5e(x, y01, y02, N, xy1, xy2)) +# endif + # if defined(FIXED_POINT) # if defined(OPUS_ARM_MAY_HAVE_NEON) diff --git a/lib/rbcodec/codecs/libopus/celt/arm/pitch_armv5e_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/pitch_armv5e_asm.S new file mode 100644 index 0000000000..ad271e9dc8 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/pitch_armv5e_asm.S @@ -0,0 +1,121 @@ +/* ARMv5E dual_inner_prod for the CELT fixed-point decoder. + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2009 Xiph.Org Foundation + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/pitch.h are met. + * + * Why this exists + * --------------- + * The decoder reaches dual_inner_prod from one place, stereo_merge, which + * calls it as dual_inner_prod(Y, X, Y, N, &xp, &side). Over eight packets of + * stereo decode that is 6,400 elements costing 12.1 cycles each, against two + * multiplies of real work. + * + * The cost is the loads. The C reads three signed halfwords per element, and + * on ARM926EJ-S an LDRSH result is two cycles late rather than one, so the + * loop stalls on every one of them. One LDR fetches two coefficients + * instead, and smlabb and smlatt take the halves apart without a separate + * sign extension -- which is also why this is worth doing here and not on + * ARMv4, where the extraction would cost exactly what the extra loads save. + * + * Alignment. A packed load needs a word-aligned address, and celt_norm is a + * halfword, so a band can start on an odd element. The three pointers can + * only be packed together if they share their alignment, so the kernel checks + * that first and falls back to the scalar loop if they disagree; when they + * agree it steps one element at a time until the address is word-aligned and + * then runs packed. Both fallbacks compute exactly what the fast path does, + * in the same order. + * + * Bit-exactness. MAC16_16 is smlabb and the sum is a plain 32-bit + * accumulation, so the order of accumulation is the only thing that could + * differ, and this keeps it: element 0 then 1 within each word, low + * addresses first. Output is bit-identical to the C, which the test harness + * checks sample by sample. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "pitch_armv5e_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 + +/* ------------------------------------------------------------------------ + * void dual_inner_prod_armv5e(const opus_val16 *x, const opus_val16 *y01, + * const opus_val16 *y02, int N, + * opus_val32 *xy1, opus_val32 *xy2) + * + * r0 = x r1 = y01 r2 = y02 r3 = N [sp+0] = xy1 [sp+4] = xy2 + * ------------------------------------------------------------------------ */ + .global dual_inner_prod_armv5e + .type dual_inner_prod_armv5e, %function +dual_inner_prod_armv5e: + push {r4-r8, lr} + mov r7, #0 @ xy01 + mov r8, #0 @ xy02 + cmp r3, #0 + ble .Ldip_done + + /* The three streams can only be read in pairs if they agree on their + alignment; two bits of exclusive-or settle it. */ + eor ip, r0, r1 + eor lr, r0, r2 + orr ip, ip, lr + tst ip, #3 + bne .Ldip_scalar + +.Ldip_align: @ step singly until x is word-aligned + tst r0, #3 + beq .Ldip_pairs + ldrsh r4, [r0], #2 + ldrsh r5, [r1], #2 + ldrsh r6, [r2], #2 + smlabb r7, r4, r5, r7 + smlabb r8, r4, r6, r8 + subs r3, r3, #1 + bne .Ldip_align + b .Ldip_done + +.Ldip_pairs: + subs r3, r3, #2 + bmi .Ldip_tail +.Ldip_loop: + ldr r4, [r0], #4 @ x[i], x[i+1] + ldr r5, [r1], #4 @ y01[i], y01[i+1] + ldr r6, [r2], #4 @ y02[i], y02[i+1] + smlabb r7, r4, r5, r7 + smlabb r8, r4, r6, r8 + smlatt r7, r4, r5, r7 + smlatt r8, r4, r6, r8 + subs r3, r3, #2 + bpl .Ldip_loop +.Ldip_tail: + tst r3, #1 @ one element left when N was odd + beq .Ldip_done + ldrsh r4, [r0] + ldrsh r5, [r1] + ldrsh r6, [r2] + smlabb r7, r4, r5, r7 + smlabb r8, r4, r6, r8 + b .Ldip_done + +.Ldip_scalar: + ldrsh r4, [r0], #2 + ldrsh r5, [r1], #2 + ldrsh r6, [r2], #2 + smlabb r7, r4, r5, r7 + smlabb r8, r4, r6, r8 + subs r3, r3, #1 + bne .Ldip_scalar + +.Ldip_done: + ldr ip, [sp, #24] @ xy1 + ldr lr, [sp, #28] @ xy2 + str r7, [ip] + str r8, [lr] + pop {r4-r8, pc} + .size dual_inner_prod_armv5e, .-dual_inner_prod_armv5e diff --git a/lib/rbcodec/codecs/libopus/config.h b/lib/rbcodec/codecs/libopus/config.h index b45aa2e8a0..418ccfd6d2 100644 --- a/lib/rbcodec/codecs/libopus/config.h +++ b/lib/rbcodec/codecs/libopus/config.h @@ -64,6 +64,7 @@ #if (ARCH_PROFILE != ARM_PROFILE_CLASSIC) #define OPUS_ARM_NO_FFT_ASM #define OPUS_ARM_NO_MDCT_ASM +#define OPUS_ARM_NO_PITCH_ASM #endif #endif From da9df96c3042c026f31d341240bd2f4a0716936b Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:28 -0400 Subject: [PATCH 235/408] opus: Good-Thomas FFT for the backward MDCT Every 48 kHz CELT length is 15 times a power of two and the factors are co-prime, so the inter-stage twiddles -- 73% of the FFT multiplies at N=480 -- vanish. New pfa_fft15 and mdct_postrot_pfa kernels on both cores; accuracy also improves 0.3 to 0.4 dB against opusdec. Modelled: -6.17% ARMv4, -1.82% ARMv5E. Measured: e200v1 42.33 -> 40.10 MHz, Clip+ 29.30 -> 28.24 MHz. Rescheduling these kernels, folded in here, measured a further -0.75% on e200v1 and -0.39% on Clip+. Co-Authored-By: Claude Opus 5 Change-Id: Ifa4a30d1045e905522828958949954615faa440d --- lib/rbcodec/codecs/libopus/SOURCES | 3 + .../codecs/libopus/celt/arm/mdct_armv4.h | 9 + .../codecs/libopus/celt/arm/mdct_armv4_asm.S | 130 +++++++- .../codecs/libopus/celt/arm/mdct_armv5e.h | 7 + .../codecs/libopus/celt/arm/mdct_armv5e_asm.S | 165 ++++++++++ lib/rbcodec/codecs/libopus/celt/arm/pfa_arm.h | 30 ++ .../codecs/libopus/celt/arm/pfa_armv4_asm.S | 286 ++++++++++++++++++ .../codecs/libopus/celt/arm/pfa_armv5e_asm.S | 227 ++++++++++++++ lib/rbcodec/codecs/libopus/celt/kiss_fft.c | 58 ++++ lib/rbcodec/codecs/libopus/celt/mdct.c | 107 +++++++ lib/rbcodec/codecs/libopus/celt/pfa.c | 148 +++++++++ lib/rbcodec/codecs/libopus/celt/pfa.h | 43 +++ lib/rbcodec/codecs/libopus/celt/pfa_tables.h | 281 +++++++++++++++++ lib/rbcodec/codecs/libopus/config.h | 8 + 14 files changed, 1488 insertions(+), 14 deletions(-) create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/pfa_arm.h create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/pfa_armv4_asm.S create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/pfa_armv5e_asm.S create mode 100644 lib/rbcodec/codecs/libopus/celt/pfa.c create mode 100644 lib/rbcodec/codecs/libopus/celt/pfa.h create mode 100644 lib/rbcodec/codecs/libopus/celt/pfa_tables.h diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index 89a0428175..53fb4c24be 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -11,12 +11,14 @@ celt/kiss_fft.c #if defined(CPU_ARM) && (ARM_ARCH == 4) celt/arm/kiss_fft_armv4_asm.S celt/arm/mdct_armv4_asm.S +celt/arm/pfa_armv4_asm.S celt/arm/comb_filter_armv4_asm.S celt/arm/denorm_armv4_asm.S celt/arm/deemph_armv4_asm.S #elif defined(CPU_ARM) && (ARM_ARCH >= 5) && (ARCH_PROFILE == ARM_PROFILE_CLASSIC) celt/arm/kiss_fft_armv5e_asm.S celt/arm/mdct_armv5e_asm.S +celt/arm/pfa_armv5e_asm.S celt/arm/pitch_armv5e_asm.S celt/arm/comb_filter_armv5e_asm.S celt/arm/denorm_armv5e_asm.S @@ -28,6 +30,7 @@ celt/arm/deemph_armv5e_asm.S celt/laplace.c celt/mathops.c celt/mdct.c +celt/pfa.c celt/modes.c celt/pitch.c celt/quant_bands.c diff --git a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h index 6c4191fa7f..f99e700f7f 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h @@ -44,6 +44,10 @@ #define mdct_prerot_opt mdct_prerot_armv4 #define mdct_postrot_opt mdct_postrot_armv4 +#ifdef OPUS_PFA +#define OVERRIDE_MDCT_POSTROT_PFA +#define mdct_postrot_pfa_opt mdct_postrot_pfa_armv4 +#endif #define mdct_mirror_opt mdct_mirror_armv4 /* step is a byte stride, so the caller scales by sizeof(kiss_fft_scalar). */ @@ -56,6 +60,11 @@ void mdct_prerot_armv4(const kiss_fft_scalar *xp1, void mdct_postrot_armv4(kiss_fft_scalar *yp0, kiss_fft_scalar *yp1, const kiss_twiddle_scalar *t, int N4, int count); +#ifdef OPUS_PFA +void mdct_postrot_pfa_armv4(const kiss_fft_scalar *S, kiss_fft_scalar *yp0, + kiss_fft_scalar *yp1, const kiss_twiddle_scalar *t, + const opus_int16 *pmap, int N4); +#endif void mdct_mirror_armv4(kiss_fft_scalar *xp1, kiss_fft_scalar *yp1, const opus_val16 *wp1, const opus_val16 *wp2, diff --git a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4_asm.S index 6612ab653c..7e264b2bf2 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4_asm.S +++ b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4_asm.S @@ -39,6 +39,16 @@ #if defined(__thumb__) || defined(__thumb2__) #error "mdct_armv4_asm.S must be assembled in ARM mode" +#endif + +/* These kernels are 2,892 bytes that carry 43% of the cycles an ARMv4 decode + * spends, which makes them the densest thing in the codec to put in IRAM. + * config.h sets OPUS_ARM_ICODE on the targets whose codec IRAM window has + * room; everywhere else this stays in .text. The firmware config.h it reads + * that from is assembly-safe, and libopus/config.h guards the rest of its + * includes with __ASSEMBLER__ so this include costs nothing here. */ +#ifdef HAVE_CONFIG_H +#include "config.h" #endif .text @@ -48,7 +58,9 @@ arm/kiss_fft_armv4.h. a is (\ar, \ai), b is (\br, \bi), \tt is scratch and the 64-bit low word. Results: \mr = a.r*b.r - a.i*b.i, \mi = a.r*b.i + a.i*b.r. \bi and \br are destroyed. \mr must differ from \ar and \br, - \mi from \ai and \tt. */ + \mi from \ai and \tt. \tt and \mr may be the same register: \tt is dead + after the fourth instruction and \mr is not written until the sixth, and + sharing them is what lets these loops keep their invariants resident. */ .macro CMULQ15 ar, ai, br, bi, tt, mr, mi smull \tt, \mi, \ai, \br smlal \tt, \mi, \ar, \bi @@ -79,28 +91,27 @@ .type mdct_prerot_armv4, %function mdct_prerot_armv4: push {r4-r11, lr} - sub sp, sp, #12 - ldr r5, [sp, #48] @ yp - ldr r6, [sp, #52] @ N4, also the loop count - ldr r4, [sp, #56] @ step, in bytes - str r4, [sp] @ the one loop-invariant reload - mov r4, r6, lsl #1 @ byte offset from t[i] to t[N4+i] + ldr r5, [sp, #36] @ yp + ldr r6, [sp, #40] @ N4, also the loop count + ldr lr, [sp, #44] @ step, in bytes + mov r4, r6, lsl #1 + sub r4, r4, #2 @ t[i] is read post-incremented, so the + @ offset on to t[N4+i] is two less cmp r6, #0 ble .Lpre_done .Lpre_loop: - ldr r7, [sp] - ldr r8, [r0], r7 @ x1 = *xp1++ - ldr r9, [r1], -r7 @ x2 = *xp2-- - ldrsh r11, [r2, r4] @ t[N4+i] + ldr r8, [r0], lr @ x1 = *xp1++ + ldr r9, [r1], -lr @ x2 = *xp2-- ldrsh r10, [r2], #2 @ t[i] - CMULQ15 r8, r9, r10, r11, lr, r7, ip + ldrsh r11, [r2, r4] @ t[N4+i], and cover t[i]'s I cycle + CMULQ15 r8, r9, r10, r11, r7, r7, ip ldrsh r10, [r3], #2 @ rev + subs r6, r6, #1 @ between the load and the address it + @ forms, where the counter is free add r10, r5, r10, lsl #3 stm r10, {r7, ip} @ yi at 2*rev, yr at 2*rev+1 - subs r6, r6, #1 bne .Lpre_loop .Lpre_done: - add sp, sp, #12 pop {r4-r11, pc} .size mdct_prerot_armv4, .-mdct_prerot_armv4 @@ -118,6 +129,14 @@ mdct_prerot_armv4: * store order here looks less symmetric than the arithmetic. * ------------------------------------------------------------------------ */ .align 2 + +/* Unreachable once the prime factor transform is built, so it gives its IRAM + back rather than sitting resident and unread. Still assembled: a size the + Good-Thomas path does not cover falls through to it, and correct-but-slow + from DRAM is the right failure mode. */ + .text + .align 2 + .global mdct_postrot_armv4 .type mdct_postrot_armv4, %function mdct_postrot_armv4: @@ -162,6 +181,10 @@ mdct_postrot_armv4: * bursts; the win is entirely the multiplier operand form. * ------------------------------------------------------------------------ */ .align 2 + + .text + .align 2 + .global mdct_mirror_armv4 .type mdct_mirror_armv4, %function mdct_mirror_armv4: @@ -184,4 +207,83 @@ mdct_mirror_armv4: pop {r4-r11, pc} .size mdct_mirror_armv4, .-mdct_mirror_armv4 + +#ifdef OPUS_PFA + .text + +/* ------------------------------------------------------------------------ + * void mdct_postrot_pfa_armv4(const kiss_fft_scalar *S, + * kiss_fft_scalar *yp0, kiss_fft_scalar *yp1, + * const kiss_twiddle_scalar *t, + * const opus_int16 *pmap, int N4) + * + * The post-rotation for the Good-Thomas transform. Same arithmetic as + * mdct_postrot_armv4, but the spectrum arrives permuted -- X[k] sits at + * M*(k mod 15) + (k mod M) -- so each end is gathered through pmap, which + * holds that index already scaled to a byte offset. One table serves both + * ends: the ascending half reads it forwards, the descending half backwards. + * + * Gathering is why this reads a different buffer than it writes, and that + * turns out to simplify the kernel rather than complicate it. The + * mixed-radix version has to store yp0[0] early and defer yp1[1] because it + * must read yp1 before overwriting it; here yp0 and yp1 are write-only, so + * both halves of each complex multiply store as soon as they exist and + * nothing stays live across the second one. + * + * No iteration counter: the two pmap pointers walk towards each other and + * the loop ends when they cross, which is the same (N4+1)>>1 iterations for + * the even N4 every CELT size has. + * + * N4*2, the offset from t[i] to t[N4+i], does fit in a register after all: + * the complex multiply's 64-bit scratch and its real result can share one, + * so the gather address register is free by the time the multiply needs a + * temporary. It used to be reloaded from the frame once per end, which cost + * six cycles an iteration for want of that observation. + * ------------------------------------------------------------------------ */ + .align 2 + .global mdct_postrot_pfa_armv4 + .type mdct_postrot_pfa_armv4, %function +mdct_postrot_pfa_armv4: + push {r4-r11, lr} + ldr r5, [sp, #36] @ pmap + ldr r6, [sp, #40] @ N4 + mov r7, r6, lsl #1 @ byte offset from t[k] to t[N4+k] + sub r6, r6, #1 + add r4, r3, r6, lsl #1 @ tb = &t[N4-1], walks down + add r6, r5, r6, lsl #1 @ pd = &pmap[N4-1], walks down + cmp r5, r6 + bhi .Lpost_pfa_done +.Lpost_pfa_loop: + /* ascending end: X[i]. The gather index leads, so the address add and + the ldm each have an unrelated load in front of them, and the pair + lands a cycle before the multiply that wants it. */ + ldrsh r12, [r5], #2 @ byte offset of X[i] + ldrsh r11, [r3, r7] @ t1 = t[N4+i] + add r12, r0, r12 + ldm r12, {r8, r9} @ im = S[2p], re = S[2p+1] + ldrsh r10, [r3], #2 @ t0 = t[i] + CMULQ15 r9, r8, r11, r10, r12, r12, lr + str lr, [r1] @ yp0[0] = yr + str r12, [r2, #4] @ yp1[1] = yi + + /* descending end: X[N4-1-i] */ + ldrsh r12, [r6], #-2 @ byte offset of X[N4-1-i] + ldrsh r11, [r4, r7] @ t1 = t[N2-i-1] + add r12, r0, r12 + ldm r12, {r8, r9} + ldrsh r10, [r4], #-2 @ t0 = t[N4-i-1] + CMULQ15 r9, r8, r11, r10, r12, r12, lr + str lr, [r2] @ yp1[0] = yr' + str r12, [r1, #4] @ yp0[1] = yi' + + add r1, r1, #8 + sub r2, r2, #8 + cmp r5, r6 + bls .Lpost_pfa_loop @ bls, not blo, so an odd N4 still gets + @ its middle pair, as the C loop does +.Lpost_pfa_done: + pop {r4-r11, pc} + .size mdct_postrot_pfa_armv4, .-mdct_postrot_pfa_armv4 +#endif /* OPUS_PFA */ + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h index 32d68ba1fa..7b8aa7b4e1 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h @@ -48,6 +48,13 @@ #define mdct_prerot_opt mdct_prerot_armv5e #define mdct_postrot_opt mdct_postrot_armv5e +#ifdef OPUS_PFA +#define OVERRIDE_MDCT_POSTROT_PFA +#define mdct_postrot_pfa_opt mdct_postrot_pfa_armv5e +void mdct_postrot_pfa_armv5e(const kiss_fft_scalar *S, kiss_fft_scalar *yp0, + kiss_fft_scalar *yp1, const kiss_twiddle_scalar *t, + const opus_int16 *pmap, int N4); +#endif #define mdct_mirror_opt mdct_mirror_armv5e /* step is a byte stride, so the caller scales by sizeof(kiss_fft_scalar). */ diff --git a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e_asm.S index 5a458813ab..c161e4d13c 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e_asm.S +++ b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e_asm.S @@ -300,5 +300,170 @@ mdct_mirror_armv5e: pop {r4-r11, pc} .size mdct_mirror_armv5e, .-mdct_mirror_armv5e +#ifdef OPUS_PFA + .text + .align 2 + + + +/* One post-rotated output. + * + * \sel picks which half of the packed twiddle pair to use + * \pm the pmap pointer, post-incremented by \pi + * \b0/\o0 where yr goes, \b1/\o1 where yi goes + * + * r10 holds t[k] packed with its neighbour and r11 holds t[N4+k] with its + * neighbour, so two loads serve two outputs. The gather is an ldrsh of the + * byte offset and one ldm, which fetches both halves of the complex value in + * a single transaction. + */ +/* The gather index arrives in r12, and this body loads the next one before + it needs its own loaded pair -- an index is a halfword, which interlocks + for two cycles if the address add follows it, and the ldm interlocks for + one if the multiply does. Rotating the load one body ahead covers both, + and it is the only place to put it: every other register is live. + + That in turn is why yr stores before yi is computed. Holding the index + in r12 costs the body its second scratch, so the accumulator has to be + free again before the imaginary half starts. + + \npm and \npi name the pointer the NEXT body reads, or none at the end of + the chain, where the loop head reloads instead. */ + .macro PR5_PFA sel, npm, npi, b0, o0, b1, o1 + add r12, r0, r12 + ldm r12, {r9, lr} @ r9 = im = S[2p], lr = re = S[2p+1]; + @ ldm fills in register order, not the + @ order written, so r9 takes the lower + @ address whichever way the list reads + .ifnc \npm, none + ldrsh r12, [\npm], #\npi @ the next body's index + .endif + smulw\sel r8, lr, r10 @ re * t0 + smlaw\sel r8, r9, r11, r8 @ + im * t1 -> yr at Q16 + mov r8, r8, lsl #1 + str r8, [\b0, #\o0] + smulw\sel r8, lr, r11 @ re * t1 + smulw\sel lr, r9, r10 @ im * t0 + sub r8, r8, lr + mov r8, r8, lsl #1 + str r8, [\b1, #\o1] + .endm + +/* The same body for the scalar fallback, which loads its own index: it runs + one output per end, so there is no next body to rotate against. */ + .macro PR5_PFA_S sel, pm, pi, b0, o0, b1, o1 + ldrsh r12, [\pm], #\pi + add r12, r0, r12 + ldm r12, {r9, lr} + smulw\sel r8, lr, r10 + smlaw\sel r8, r9, r11, r8 + smulw\sel r12, lr, r11 + smulw\sel lr, r9, r10 + mov r8, r8, lsl #1 + sub r12, r12, lr + str r8, [\b0, #\o0] + mov r12, r12, lsl #1 + str r12, [\b1, #\o1] + .endm + +/* ------------------------------------------------------------------------ + * void mdct_postrot_pfa_armv5e(const kiss_fft_scalar *S, + * kiss_fft_scalar *yp0, kiss_fft_scalar *yp1, + * const kiss_twiddle_scalar *t, + * const opus_int16 *pmap, int N4) + * + * The post-rotation for the Good-Thomas transform. Same arithmetic as + * mdct_postrot_armv5e; what changes is that the spectrum arrives permuted, + * X[k] at M*(k mod 15) + (k mod M), so each end is gathered through pmap, + * which already holds that index as a byte offset. The ascending half reads + * the table forwards and the descending half backwards. + * + * Gathering makes yp0 and yp1 write-only, and that is what lets the twiddles + * stay packed. A kiss_twiddle_scalar is 16 bits, so t[k] and t[k+1] are one + * aligned word; the mixed-radix kernel exploits that by holding all four + * twiddle words -- both ends at once -- across a body that does two outputs + * from each. Here two of those registers are spent on the pmap pointers, so + * instead the body does both ascending outputs, reloads the pair for the + * descending end, and does both of those. That reordering is legal only + * because nothing is read back: in the in-place version the descending + * output has to be read before the ascending one overwrites it, which forces + * the two ends to interleave. + * + * No iteration counter either: the two pmap pointers walk towards each other + * and the loop ends when they cross. Four outputs an iteration and N4 a + * multiple of four -- 60, 120, 240, 480 -- means that lands exactly. + * + * The scalar path below is the fallback for a trig table the packed loads + * cannot address as words, or an N4 the unroll does not divide. No CELT + * mode reaches it; build with -DMDCT_ARMV5E_FORCE_SCALAR to route everything + * through it and confirm it still agrees. + * ------------------------------------------------------------------------ */ + .align 2 + .global mdct_postrot_pfa_armv5e + .type mdct_postrot_pfa_armv5e, %function +mdct_postrot_pfa_armv5e: + push {r4-r11, lr} + ldr r5, [sp, #36] @ pmap + ldr r12, [sp, #40] @ N4 + mov r7, r12, lsl #1 @ byte offset from t[k] to t[N4+k] + sub r8, r12, #1 + add r4, r3, r8, lsl #1 @ tb = &t[N4-1], walks down + add r6, r5, r8, lsl #1 @ pd = &pmap[N4-1], walks down + + /* The packed path needs the trig table word-addressable and N4 a + multiple of four; the second gives the first its stride and lets the + crossing test land exactly. */ + orr r8, r3, r12 +#ifdef MDCT_ARMV5E_FORCE_SCALAR + orr r8, r8, #1 +#endif + tst r8, #3 + bne .Lpost5_pfa_scalar + + cmp r5, r6 + bhi .Lpost5_pfa_done +.Lpost5_pfa_pairs: + ldrsh r12, [r5], #2 @ index of X[i]; the twiddle loads below + @ stand between it and the address add + ldr r10, [r3] @ t[i] : t[i+1] + ldr r11, [r3, r7] @ t[N4+i] : t[N4+i+1] + add r3, r3, #4 + PR5_PFA b, r5, 2, r1, 0, r2, 4 @ X[i] + PR5_PFA t, r6, -2, r1, 8, r2, -4 @ X[i+1] + + add lr, r4, r7 + ldr r10, [r4, #-2] @ t[N4-i-2] : t[N4-i-1] + ldr r11, [lr, #-2] @ t[N2-i-2] : t[N2-i-1] + sub r4, r4, #4 + PR5_PFA t, r6, -2, r2, 0, r1, 4 @ X[N4-1-i] + PR5_PFA b, none, 0, r2, -8, r1, 12 @ X[N4-2-i] + + add r1, r1, #16 + sub r2, r2, #16 + cmp r5, r6 + blo .Lpost5_pfa_pairs + b .Lpost5_pfa_done + +/* One output per end per iteration, twiddles a halfword at a time. The + crossing test is bls rather than blo so an odd N4 still computes its + middle pair, which is what the C loop to (N4+1)>>1 does. */ +.Lpost5_pfa_scalar: + cmp r5, r6 + bhi .Lpost5_pfa_done +.Lpost5_pfa_sloop: + ldrsh r11, [r3, r7] @ t[N4+i] + ldrsh r10, [r3], #2 @ t[i] + PR5_PFA_S b, r5, 2, r1, 0, r2, 4 + ldrsh r11, [r4, r7] @ t[N2-i-1] + ldrsh r10, [r4], #-2 @ t[N4-i-1] + PR5_PFA_S b, r6, -2, r2, 0, r1, 4 + add r1, r1, #8 + sub r2, r2, #8 + cmp r5, r6 + bls .Lpost5_pfa_sloop +.Lpost5_pfa_done: + pop {r4-r11, pc} + .size mdct_postrot_pfa_armv5e, .-mdct_postrot_pfa_armv5e +#endif .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/arm/pfa_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/pfa_arm.h new file mode 100644 index 0000000000..d5910a3b86 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/pfa_arm.h @@ -0,0 +1,30 @@ +/* ARM hooks for the Good-Thomas 15-point kernel. + + Copyright (c) 2026 Michael Giacomelli + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the conditions stated in + celt/kiss_fft.c are met. +*/ +#ifndef PFA_ARM_H +#define PFA_ARM_H + +#if defined(FIXED_POINT) && defined(OPUS_PFA) + +#if defined(OPUS_ARM_INLINE_EDSP) + +#define OVERRIDE_PFA_FFT15 +void pfa_fft15_armv5e(const kiss_fft_cpx *in, kiss_fft_cpx *out, int ostride); +#define PFA_FFT15(in, out, ostride) pfa_fft15_armv5e(in, out, ostride) + +#elif defined(OPUS_ARM_INLINE_ASM) + +#define OVERRIDE_PFA_FFT15 +void pfa_fft15_armv4(const kiss_fft_cpx *in, kiss_fft_cpx *out, int ostride); +#define PFA_FFT15(in, out, ostride) pfa_fft15_armv4(in, out, ostride) + +#endif + +#endif /* FIXED_POINT && OPUS_PFA */ + +#endif /* PFA_ARM_H */ diff --git a/lib/rbcodec/codecs/libopus/celt/arm/pfa_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/pfa_armv4_asm.S new file mode 100644 index 0000000000..ce0b2ae9f0 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/pfa_armv4_asm.S @@ -0,0 +1,286 @@ +/* ARMv4 Good-Thomas 15-point DFT for the CELT backward MDCT. + * + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/kiss_fft.c are met. + * + * Why this exists + * --------------- + * 15 = 3*5 and the two are co-prime, so this is Good-Thomas inside + * Good-Thomas: five 3-point DFTs, then three 5-point DFTs, and nothing + * between them. A 15-point complex DFT for 40 real multiplies, where the + * mixed-radix chain it replaces spends 16 twiddle multiplies per point on + * the radix-5 pass alone. + * + * The arithmetic is the arithmetic the existing butterflies already use, so + * nothing new is being claimed about accuracy: epi3 is the radix-3 constant + * from kf_bfly3, and the radix-5 half keeps the sqrt(5)/4 collapse from + * kf_bfly5, where cos(2pi/5)+cos(4pi/5) is exactly -1/2 -- and the Q15 + * constants honour it exactly, 10126 - 26510 == -16384 -- so four cosine + * products become one multiply by yc. Q15 narrowing is the exact + * smull/shift/orr form the other ARMv4 kernels use, which keeps all 32 bits + * of the product where MULT16_32_Q15_armv4 drops the low one. + * + * Register pressure is the whole difficulty. A 5-point DFT needs eight live + * words across its eight multiplies plus two scratch, and ARM has fourteen + * registers to hold that, the output pointer, its stride and two constants. + * It fits only because the sum and difference pairs s5 and s11 are parked on + * the frame across the multiplies -- sixteen bytes, to the same cache line + * every call -- and because yc borrows the register ya.i will need later, + * reloading it afterwards for the price of one ldr per 5-point DFT. + * + * The output permutation costs nothing. Output k2 of the k1'th 5-point DFT + * belongs at 15-point index (10*k1 + 6*k2) mod 15; walking that from k1 in + * steps of 3 visits every one of the five, so the destinations are a single + * post-incremented pointer and the permutation is only a choice of which + * register to store. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "pfa_armv4_asm.S must be assembled in ARM mode" +#endif + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + + .text + .align 2 + +/* Frame: v[15] complex at 0..119, the s5/s11 park at 120..135, then the + saved output pointer, the 3-element output step and the 5-element stride + the three 5-point bases are derived from. */ +#define VOFF 0 +#define PARK 120 +#define SV_OUT 136 +#define SV_STEP3 140 +#define SV_FIVE 144 +#define FRAME 152 + +/* Q15 narrowing that keeps every bit of the product: see kiss_fft_armv4.h. */ + .macro QNARROW dst, lo, hi + mov \lo, \lo, lsr #15 + orr \dst, \lo, \hi, lsl #17 + .endm + +/* Advance the output pointer by d groups of three elements. The five + destinations of one 5-point DFT are (10*k1 + 6*k2) mod 15, which walking + k2 in the order 0,1,4,2,3 visits as steps of 3 with one wrap, so every + delta is a small multiple of r3 and none needs a multiply. */ + .macro PFA_ADV d + .if \d == 2 + add r0, r0, r3, lsl #1 + .elseif \d == 1 + add r0, r0, r3 + .elseif \d == -3 + sub r0, r0, r3, lsl #1 + sub r0, r0, r3 + .elseif \d == -4 + sub r0, r0, r3, lsl #2 + .else + .error "unexpected PFA store delta" + .endif + .endm + +/* One 5-point DFT over v[voff..voff+4], writing through r0. + * + * r0 = output, already at this DFT's base r1 = ya.i r2 = yb.i + * r3 = three output elements, in bytes + * + * Ten multiplies: two for the sqrt(5)/4 term that stands in for the four + * cosine products, eight for the sines. The four sine products of each + * half share their operands between s6 and s12, so both come out of one + * pass over s9 and s10 and neither has to be kept alive across the other. + */ + .macro PFA_DFT5 voff, d1, d2, d3, d4 + add r12, sp, #(\voff+8) + ldmia r12, {r4,r5,r6,r7,r8,r9} @ x1, x2, x3 + add r12, sp, #(\voff+32) + ldm r12, {r10,r11} @ x4 + + add r12, r4, r10 @ s7.r = x1.r + x4.r + sub r4, r4, r10 @ s10.r = x1.r - x4.r + add r10, r5, r11 @ s7.i + sub r5, r5, r11 @ s10.i + add r11, r6, r8 @ s8.r = x2.r + x3.r + sub r6, r6, r8 @ s9.r = x2.r - x3.r + add r8, r7, r9 @ s8.i + sub r7, r7, r9 @ s9.i + + /* s4 *= sqrt(5)/4. yc borrows ya.i's register; one ldr puts it back, + which is cheaper than spending a register on a constant used twice. + It loads ahead of the four sums below, which is what keeps its I + cycle off the multiply. */ + ldr r1, .Lpfa_yc + add r9, r12, r11 @ s3.r = s7.r + s8.r + sub r12, r12, r11 @ s4.r = s7.r - s8.r + add r11, r10, r8 @ s3.i + sub r10, r10, r8 @ s4.i + + smull r8, lr, r12, r1 + QNARROW r12, r8, lr @ s4.r + smull r8, lr, r10, r1 + QNARROW r10, r8, lr @ s4.i + ldr lr, [sp, #(\voff+0)] @ x0.r + ldr r1, .Lpfa_yai @ and this covers its I cycle + + /* X0 = x0 + s3 goes out immediately, then s3 becomes x0 - s3/4. x0.r + is loaded a line early so the constant reload above covers it, and + x0.i goes into the scratch that storing X0.r frees, which puts three + instructions between that load and its use. */ + add r8, lr, r9 @ X0.r + str r8, [r0] + ldr r8, [sp, #(\voff+4)] @ x0.i + add r9, r9, #2 + sub r9, lr, r9, asr #2 @ QUARTER_OF rounds, as kf_bfly5 does + add lr, r8, r11 @ X0.i + str lr, [r0, #4] + add r11, r11, #2 + sub r11, r8, r11, asr #2 + + /* s5 and s11 are not touched by the eight multiplies below and there is + no register to keep them in, so they wait on the frame -- sixteen + bytes, the same line every call. */ + add r8, r9, r12 @ s5.r + sub r9, r9, r12 @ s11.r + add lr, r11, r10 @ s5.i + sub r11, r11, r10 @ s11.i + mov r10, lr @ stm stores in register order, not + add r12, sp, #PARK @ list order, so s5.i must sit in r10 + stmia r12, {r8, r9, r10, r11} + + /* s6.r = s10.i*ya.i + s9.i*yb.i s12.r = s9.i*ya.i - s10.i*yb.i */ + smull r10, r12, r5, r1 + QNARROW r8, r10, r12 + smull r10, r12, r7, r2 + QNARROW r9, r10, r12 + smull r10, r12, r7, r1 + QNARROW r11, r10, r12 + smull r10, r12, r5, r2 + QNARROW lr, r10, r12 + add r5, r8, r9 @ s6.r + sub r7, r11, lr @ s12.r + + /* s6.i = -(s10.r*ya.i + s9.r*yb.i) s12.i = s10.r*yb.i - s9.r*ya.i */ + smull r10, r12, r4, r1 + QNARROW r8, r10, r12 + smull r10, r12, r6, r2 + QNARROW r9, r10, r12 + smull r10, r12, r4, r2 + QNARROW r11, r10, r12 + smull r10, r12, r6, r1 + QNARROW lr, r10, r12 + add r4, r8, r9 + rsb r4, r4, #0 @ s6.i + sub r6, r11, lr @ s12.i + + add r12, sp, #PARK + ldmia r12, {r8, r9, r10, r11} @ s5.r, s11.r, s5.i, s11.i + + sub r12, r8, r5 @ X1 = s5 - s6 + sub lr, r10, r4 + PFA_ADV \d1 + stmia r0, {r12, lr} + + add r12, r8, r5 @ X4 = s5 + s6 + add lr, r10, r4 + PFA_ADV \d2 + stmia r0, {r12, lr} + + add r12, r9, r7 @ X2 = s11 + s12 + add lr, r11, r6 + PFA_ADV \d3 + stmia r0, {r12, lr} + + sub r12, r9, r7 @ X3 = s11 - s12 + sub lr, r11, r6 + PFA_ADV \d4 + stmia r0, {r12, lr} + .endm + +/* ------------------------------------------------------------------------ + * void pfa_fft15_armv4(const kiss_fft_cpx *in, kiss_fft_cpx *out, + * int ostride) + * + * r0 = in (15 contiguous complex) r1 = out r2 = ostride, in elements + * ------------------------------------------------------------------------ */ + .global pfa_fft15_armv4 + .type pfa_fft15_armv4, %function +pfa_fft15_armv4: + push {r4-r11, lr} + sub sp, sp, #FRAME + + mov r2, r2, lsl #3 @ ostride in bytes + str r1, [sp, #SV_OUT] + add r3, r2, r2, lsl #1 @ 3 elements + str r3, [sp, #SV_STEP3] + add r3, r2, r2, lsl #2 @ 5 elements + str r3, [sp, #SV_FIVE] + +/* ---- Five 3-point DFTs ------------------------------------------------- + * Group g reads in[3g..3g+2] and writes v[g], v[5+g], v[10+g], so the + * 5-point DFTs below each read a contiguous run of five. + */ + add r1, sp, #VOFF @ &v[0] + add r2, sp, #(VOFF+40) @ &v[5] + add r3, sp, #(VOFF+80) @ &v[10] + ldr lr, .Lpfa_epi3 + mov r12, #5 + +.Lpfa_b3: + ldmia r0!, {r4,r5,r6,r7,r8,r9} @ x0, x1, x2 + add r10, r6, r8 @ s3.r = x1.r + x2.r + sub r6, r6, r8 @ s0.r = x1.r - x2.r + add r11, r7, r9 @ s3.i + sub r7, r7, r9 @ s0.i + sub r8, r4, r10, asr #1 @ xm.r = x0.r - HALF_OF(s3.r) + sub r9, r5, r11, asr #1 @ xm.i + add r4, r4, r10 @ X0.r = x0.r + s3.r + add r5, r5, r11 @ X0.i + smull r10, r11, r6, lr @ s0.r * epi3 + QNARROW r6, r10, r11 + smull r10, r11, r7, lr @ s0.i * epi3 + QNARROW r7, r10, r11 + stmia r1!, {r4,r5} @ v[g] + sub r10, r8, r7 + add r11, r9, r6 + stmia r2!, {r10,r11} @ v[5+g] + add r10, r8, r7 + sub r11, r9, r6 + stmia r3!, {r10,r11} @ v[10+g] + subs r12, r12, #1 + bne .Lpfa_b3 + +/* ---- Three 5-point DFTs ------------------------------------------------ */ + ldr r1, .Lpfa_yai + ldr r2, .Lpfa_ybi + ldr r3, [sp, #SV_STEP3] + + ldr r0, [sp, #SV_OUT] @ k1 = 0: base is out itself + PFA_DFT5 (VOFF+0), 2, 1, 1, -3 + + ldr r0, [sp, #SV_OUT] + ldr r12, [sp, #SV_FIVE] + add r0, r0, r12, lsl #1 @ k1 = 1: base is out + 10 elements + PFA_DFT5 (VOFF+40), -3, 1, 1, 2 + + ldr r0, [sp, #SV_OUT] + ldr r12, [sp, #SV_FIVE] + add r0, r0, r12 @ k1 = 2: base is out + 5 elements + PFA_DFT5 (VOFF+80), 2, 1, -4, 2 + + add sp, sp, #FRAME + pop {r4-r11, pc} + .size pfa_fft15_armv4, .-pfa_fft15_armv4 + + .align 2 +.Lpfa_epi3: + .word -28378 @ -sin(2*pi/3), as kf_bfly3 uses +.Lpfa_yai: + .word -31164 @ ya.i +.Lpfa_ybi: + .word -19261 @ yb.i +.Lpfa_yc: + .word 18318 @ sqrt(5)/4 diff --git a/lib/rbcodec/codecs/libopus/celt/arm/pfa_armv5e_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/pfa_armv5e_asm.S new file mode 100644 index 0000000000..21d1419086 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/pfa_armv5e_asm.S @@ -0,0 +1,227 @@ +/* ARMv5E Good-Thomas 15-point DFT for the CELT backward MDCT. + * + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/kiss_fft.c are met. + * + * Why this exists + * --------------- + * The same transform as pfa_armv4_asm.S -- five 3-point DFTs then three + * 5-point DFTs, co-prime so nothing sits between them -- but this core + * changes what the arithmetic costs, and with it the register budget. + * + * A Q15 narrowing is one smulwb rather than smull/shift/orr, so each of the + * forty multiplies drops from three instructions to one and needs one + * scratch register rather than a pair. Where the C adds two products, + * smlawb folds the add in and is bit-exact with it, because both truncate + * at Q16 before the sum and the shift distributes. Where it subtracts them + * the two stay separate: smlaw only adds, so fusing would mean negating a + * twiddle, and truncation is not symmetric about zero. + * + * The Q15 doubling is deferred. MULT16_32_Q15 on this core is smulwb + * followed by a shift left, and every consumer of a product here is an add + * or a subtract, so the shift folds into it as a free immediate. That is + * exact rather than approximate: a +/- (b << 1) is the same value as + * a +/- SHL32(b, 1) in all 32 bits. + * + * Between them those two savings free enough registers that the 5-point DFT + * needs no frame at all -- where the ARMv4 version has to park its sum and + * difference pair across the multiplies, this one keeps everything live. + * ya.i and yb.i share one word, picked apart by the b and t halves of + * smulwb and smulwt, which is the same packing the radix-5 pass in + * kiss_fft_armv5e_asm.S uses. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "pfa_armv5e_asm.S must be assembled in ARM mode" +#endif + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + + .text + .align 2 + +#define VOFF 0 +#define SV_OUT 120 +#define SV_STEP3 124 +#define SV_FIVE 128 +#define FRAME 136 + +/* Advance the output pointer by d groups of three elements; see the ARMv4 + kernel for why every delta is a small multiple of one stride. */ + .macro PFA5_ADV d + .if \d == 2 + add r0, r0, r3, lsl #1 + .elseif \d == 1 + add r0, r0, r3 + .elseif \d == -3 + sub r0, r0, r3, lsl #1 + sub r0, r0, r3 + .elseif \d == -4 + sub r0, r0, r3, lsl #2 + .else + .error "unexpected PFA store delta" + .endif + .endm + +/* One 5-point DFT over v[voff..voff+4]. + * r0 = output, at this DFT's base r1 = ya.i packed with yb.i + * r2 = sqrt(5)/4 r3 = three output elements, in bytes + */ + .macro PFA5_DFT5 voff, d1, d2, d3, d4 + add r12, sp, #(\voff+8) + ldmia r12, {r4,r5,r6,r7,r8,r9} @ x1, x2, x3 + add r12, sp, #(\voff+32) + ldm r12, {r10,r11} @ x4 + + add r12, r4, r10 @ s7.r = x1.r + x4.r + sub r4, r4, r10 @ s10.r = x1.r - x4.r + add r10, r5, r11 @ s7.i + sub r5, r5, r11 @ s10.i + add r11, r6, r8 @ s8.r = x2.r + x3.r + sub r6, r6, r8 @ s9.r = x2.r - x3.r + add r8, r7, r9 @ s8.i + sub r7, r7, r9 @ s9.i + + add r9, r12, r11 @ s3.r = s7.r + s8.r + sub r12, r12, r11 @ s4.r = s7.r - s8.r + add r11, r10, r8 @ s3.i + sub r10, r10, r8 @ s4.i + + ldr lr, [sp, #(\voff+0)] @ x0.r, ahead of the two multiplies + smulwb r12, r12, r2 @ s4.r * sqrt(5)/4, left at Q16 + smulwb r10, r10, r2 @ s4.i * sqrt(5)/4 + + add r8, lr, r9 @ X0.r + str r8, [r0] + ldr r8, [sp, #(\voff+4)] @ x0.i, into the scratch that frees + add r9, r9, #2 + sub r9, lr, r9, asr #2 @ QUARTER_OF rounds, as kf_bfly5 does + add lr, r8, r11 @ X0.i + str lr, [r0, #4] + add r11, r11, #2 + sub r11, r8, r11, asr #2 + + add r8, r9, r12, lsl #1 @ s5.r (the deferred doubling) + sub r9, r9, r12, lsl #1 @ s11.r + add lr, r11, r10, lsl #1 @ s5.i + sub r11, r11, r10, lsl #1 @ s11.i + + /* s6.r = s10.i*ya + s9.i*yb, so smlawt carries the add. */ + smulwb r10, r5, r1 + smlawt r10, r7, r1, r10 + /* s12.r = s9.i*ya - s10.i*yb: a difference, so the products stay apart. */ + smulwb r12, r7, r1 + smulwt r5, r5, r1 + sub r12, r12, r5 + + /* s6.i is the negation of this sum; the sign folds into the consumers. */ + smulwb r5, r4, r1 + smlawt r5, r6, r1, r5 + /* s12.i = s10.r*yb - s9.r*ya */ + smulwt r4, r4, r1 + smulwb r6, r6, r1 + sub r4, r4, r6 + + sub r6, r8, r10, lsl #1 @ X1.r = s5.r - s6.r + add r7, lr, r5, lsl #1 @ X1.i = s5.i - s6.i + PFA5_ADV \d1 + stmia r0, {r6, r7} + + add r6, r8, r10, lsl #1 @ X4.r + sub r7, lr, r5, lsl #1 @ X4.i + PFA5_ADV \d2 + stmia r0, {r6, r7} + + add r6, r9, r12, lsl #1 @ X2.r = s11.r + s12.r + add r7, r11, r4, lsl #1 @ X2.i + PFA5_ADV \d3 + stmia r0, {r6, r7} + + sub r6, r9, r12, lsl #1 @ X3.r + sub r7, r11, r4, lsl #1 @ X3.i + PFA5_ADV \d4 + stmia r0, {r6, r7} + .endm + +/* ------------------------------------------------------------------------ + * void pfa_fft15_armv5e(const kiss_fft_cpx *in, kiss_fft_cpx *out, + * int ostride) + * ------------------------------------------------------------------------ */ + .global pfa_fft15_armv5e + .type pfa_fft15_armv5e, %function +pfa_fft15_armv5e: + push {r4-r11, lr} + sub sp, sp, #FRAME + + mov r2, r2, lsl #3 @ ostride in bytes + str r1, [sp, #SV_OUT] + add r3, r2, r2, lsl #1 + str r3, [sp, #SV_STEP3] + add r3, r2, r2, lsl #2 + str r3, [sp, #SV_FIVE] + +/* ---- Five 3-point DFTs ------------------------------------------------ */ + add r1, sp, #VOFF + add r2, sp, #(VOFF+40) + add r3, sp, #(VOFF+80) + ldr lr, .Lpfa5_epi3 + mov r12, #5 + +.Lpfa5_b3: + ldmia r0!, {r4,r5,r6,r7,r8,r9} @ x0, x1, x2 + add r10, r6, r8 @ s3.r + sub r6, r6, r8 @ s0.r + add r11, r7, r9 @ s3.i + sub r7, r7, r9 @ s0.i + sub r8, r4, r10, asr #1 @ xm.r = x0.r - HALF_OF(s3.r) + sub r9, r5, r11, asr #1 @ xm.i + add r4, r4, r10 @ X0.r + add r5, r5, r11 @ X0.i + smulwb r6, r6, lr @ s0.r * epi3, left at Q16 + smulwb r7, r7, lr @ s0.i * epi3 + stmia r1!, {r4,r5} @ v[g] + sub r10, r8, r7, lsl #1 + add r11, r9, r6, lsl #1 + stmia r2!, {r10,r11} @ v[5+g] + add r10, r8, r7, lsl #1 + sub r11, r9, r6, lsl #1 + stmia r3!, {r10,r11} @ v[10+g] + subs r12, r12, #1 + bne .Lpfa5_b3 + +/* ---- Three 5-point DFTs ----------------------------------------------- */ + ldr r1, .Lpfa5_yab + ldr r2, .Lpfa5_yc + ldr r3, [sp, #SV_STEP3] + + ldr r0, [sp, #SV_OUT] + PFA5_DFT5 (VOFF+0), 2, 1, 1, -3 + + ldr r0, [sp, #SV_OUT] + ldr r12, [sp, #SV_FIVE] + add r0, r0, r12, lsl #1 + PFA5_DFT5 (VOFF+40), -3, 1, 1, 2 + + ldr r0, [sp, #SV_OUT] + ldr r12, [sp, #SV_FIVE] + add r0, r0, r12 + PFA5_DFT5 (VOFF+80), 2, 1, -4, 2 + + add sp, sp, #FRAME + pop {r4-r11, pc} + .size pfa_fft15_armv5e, .-pfa_fft15_armv5e + + .align 2 +.Lpfa5_epi3: + .word -28378 @ -sin(2*pi/3), in the low half +.Lpfa5_yab: + .word 0xb4c38644 @ ya.i = -31164 low, yb.i = -19261 top +.Lpfa5_yc: + .word 18318 @ sqrt(5)/4 + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/kiss_fft.c b/lib/rbcodec/codecs/libopus/celt/kiss_fft.c index 69e85b981a..664f14e4cf 100644 --- a/lib/rbcodec/codecs/libopus/celt/kiss_fft.c +++ b/lib/rbcodec/codecs/libopus/celt/kiss_fft.c @@ -40,6 +40,10 @@ #include "os_support.h" #include "mathops.h" #include "stack_alloc.h" +#ifdef OPUS_PFA +#include "pfa.h" +#include "pfa_tables.h" +#endif /* The guts header contains all the multiplication and addition macros that are defined for complex numbers. It also delares the kf_ internal functions. @@ -619,6 +623,60 @@ void opus_fft_impl(const kiss_fft_state *st,kiss_fft_cpx *fout) } } +#ifdef OPUS_PFA +/* kf_bfly4 reads nothing from the state but the twiddle table, and the prime + factor sub-transforms want their own: one 32-entry table serves all four + sizes, read with a stride, exactly as kiss_fft shares a single table + between the four transform lengths. */ +static const kiss_fft_state pfa_st32 = { 0, 0, 0, 0, {0}, 0, pfa_tw32, 0 }; + +void opus_pfa_impl(const kiss_fft_cpx *fin, + kiss_fft_cpx *fout, int nfft) +{ + const opus_int16 *brev; + int M = nfft/15; + int n2; + + switch (nfft) + { + case 60: brev = pfa_brev_4; break; + case 120: brev = pfa_brev_8; break; + case 240: brev = pfa_brev_16; break; + default: brev = pfa_brev_32; break; + } + + /* Pass 1: M fifteen-point DFTs. The bit-reversal the radix-4 chain below + expects is folded into this scatter, so pass 2 needs no permutation of + its own. */ + for (n2=0;n2 #include "os_support.h" #include "mathops.h" +#ifdef OPUS_PFA +#include "pfa.h" +#include "pfa_tables.h" + +/* The pre-rotation already scatters through a table; for the prime factor + transform it reads this one instead of the bit-reversal, which is why the + gather costs nothing. */ +static const opus_int16 *pfa_gather(int n) +{ + switch (n) + { + case 60: return pfa_in_60; + case 120: return pfa_in_120; + case 240: return pfa_in_240; + default: return pfa_in_480; + } +} + +/* Where the post-rotation finds X[k], as a byte offset. */ +static const opus_int16 *pfa_postmap(int n) +{ + switch (n) + { + case 60: return pfa_post_60; + case 120: return pfa_post_120; + case 240: return pfa_post_240; + default: return pfa_post_480; + } +} + +/* Walk p(k) = M*(k mod 15) + (k mod M) by one step. Both residues are + running counters, so the post-rotation needs no index table. */ +#define PFA_ADV_UP(p, k1, k2, M) do { (p) += (M)+1; if (++(k1) == 15) { (k1) = 0; (p) -= 15*(M); } if (++(k2) == (M)) { (k2) = 0; (p) -= (M); } } while (0) +#define PFA_ADV_DN(p, k1, k2, M) do { (p) -= (M)+1; if ((k1)-- == 0) { (k1) = 14; (p) += 15*(M); } if ((k2)-- == 0) { (k2) = (M)-1; (p) += (M); } } while (0) +#endif #if defined(OPUS_ARM_ASM) #include "arm/mdct_armv4.h" #include "arm/mdct_armv5e.h" @@ -249,6 +284,12 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca int i; int N, N2, N4; const kiss_twiddle_scalar *trig; +#ifdef OPUS_PFA + int use_pfa; + kiss_fft_cpx *pfa_out = NULL; + VARDECL(kiss_fft_cpx, pfa_tmp); +#endif + SAVE_STACK; (void) arch; N = l->n; @@ -261,6 +302,18 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca N2 = N>>1; N4 = N>>2; +#ifdef OPUS_PFA + /* Pass 1 of the prime factor transform reads fifteen contiguous points + and writes them strided, so it cannot work in place. The input buffer + is exactly the right size and is documented as destroyed here, so for + the usual stride==1 call it costs nothing; a strided call cannot reuse + it, but those are always the short transform, N4 == 60. */ + use_pfa = OPUS_PFA_SIZE(N4) && (stride == 1 || N4 == 60); + ALLOC(pfa_tmp, (use_pfa && stride != 1) ? N4 : ALLOC_NONE, kiss_fft_cpx); + if (use_pfa) + pfa_out = (stride == 1) ? (kiss_fft_cpx *)in : pfa_tmp; +#endif + /* Pre-rotate */ { /* Temp pointers to make it really clear to the compiler what we're doing */ @@ -269,6 +322,9 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca kiss_fft_scalar * OPUS_RESTRICT yp = out+(overlap>>1); const kiss_twiddle_scalar * OPUS_RESTRICT t = &trig[0]; const opus_int16 * OPUS_RESTRICT bitrev = l->kfft[shift]->bitrev; +#ifdef OPUS_PFA + if (use_pfa) bitrev = pfa_gather(N4); +#endif #ifdef OVERRIDE_MDCT_PREROT mdct_prerot_opt(xp1, xp2, t, bitrev, yp, N4, 2*stride*(int)sizeof(kiss_fft_scalar)); @@ -290,6 +346,11 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca #endif } +#ifdef OPUS_PFA + if (use_pfa) + opus_pfa_impl((const kiss_fft_cpx*)(out+(overlap>>1)), pfa_out, N4); + else +#endif opus_fft_impl(l->kfft[shift], (kiss_fft_cpx*)(out+(overlap>>1))); /* Post-rotate and de-shuffle from both ends of the buffer at once to make @@ -300,6 +361,51 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca const kiss_twiddle_scalar *t = &trig[0]; /* Loop to (N4+1)>>1 to handle odd N4. When N4 is odd, the middle pair will be computed twice. */ +#ifdef OPUS_PFA + if (use_pfa) + { + /* Same rotation, but the transform is in Good-Thomas order, so the + two ends are gathered through p(k) rather than read in place. + That is also why this writes a different buffer than it reads. */ + const kiss_fft_scalar *S = (const kiss_fft_scalar *)pfa_out; + int M = N4/15; + int pa = 0, ka = 0, qa = 0; + int kd = (N4-1)%15, qd = (N4-1)&(M-1); + int pd = M*kd + qd; +#ifdef OVERRIDE_MDCT_POSTROT_PFA + mdct_postrot_pfa_opt(S, yp0, yp1, t, pfa_postmap(N4), N4); + (void)pa; (void)ka; (void)qa; (void)pd; (void)kd; (void)qd; (void)M; +#else + for(i=0;i<(N4+1)>>1;i++) + { + kiss_fft_scalar re, im, yr, yi; + kiss_twiddle_scalar t0, t1; + re = S[2*pa+1]; + im = S[2*pa]; + t0 = t[i]; + t1 = t[N4+i]; + yr = ADD32_ovflw(S_MUL(re,t0), S_MUL(im,t1)); + yi = SUB32_ovflw(S_MUL(re,t1), S_MUL(im,t0)); + re = S[2*pd+1]; + im = S[2*pd]; + yp0[0] = yr; + yp1[1] = yi; + + t0 = t[(N4-i-1)]; + t1 = t[(N2-i-1)]; + yr = ADD32_ovflw(S_MUL(re,t0), S_MUL(im,t1)); + yi = SUB32_ovflw(S_MUL(re,t1), S_MUL(im,t0)); + yp1[0] = yr; + yp0[1] = yi; + yp0 += 2; + yp1 -= 2; + PFA_ADV_UP(pa, ka, qa, M); + PFA_ADV_DN(pd, kd, qd, M); + } +#endif + } + else +#endif #ifdef OVERRIDE_MDCT_POSTROT mdct_postrot_opt(yp0, yp1, t, N4, (N4+1)>>1); #else @@ -356,5 +462,6 @@ void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_sca } #endif } + RESTORE_STACK; } #endif /* OVERRIDE_clt_mdct_backward */ diff --git a/lib/rbcodec/codecs/libopus/celt/pfa.c b/lib/rbcodec/codecs/libopus/celt/pfa.c new file mode 100644 index 0000000000..daca7c82e4 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/pfa.c @@ -0,0 +1,148 @@ +/* Good-Thomas (prime factor) FFT for the CELT backward MDCT. + * + * Copyright (c) 2007-2008 CSIRO + * Copyright (c) 2007-2008 Xiph.Org Foundation + * Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/kiss_fft.c are met. + * + * Why this exists + * --------------- + * Every FFT length a 48 kHz Opus stream uses is 15 times a power of two: + * 60, 120, 240, 480. 15 and 2^k are co-prime, so the prime factor algorithm + * applies, and its whole point is that co-prime factors need no twiddle + * factors between them -- only an index permutation. + * + * That matters because in the mixed-radix decomposition kiss_fft picks + * (5, 3, 4, 2, 4 for N=480) the inter-stage twiddles are 4,020 of the 5,540 + * real multiplies, 73% of the total. Splitting 480 as 15 x 32 deletes that + * class of work rather than making it cheaper: 2,780 multiplies, half as + * many. Multiplies are 28.4% of ARMv4 decode, so this is the largest single + * item the profile still offers on that CPU. + * + * Structure, for N = 15*M: + * + * gather x[n] -> u[15*n2 + 3*m2 + m1], a table the pre-rotation reads + * in place of the bit-reversal it read before, so it is free + * pass 1 M independent 15-point DFTs, contiguous in, strided M out + * pass 2 15 independent M-point DFTs, contiguous, which is exactly + * what the existing kf_bfly4/kf_bfly2 kernels already do + * scatter X[k] = B[k mod 15][k mod M], absorbed into the post-rotation + * as two running counters, no table + * + * Buffers. Pass 1 cannot be in place (it reads 15 contiguous and writes 15 + * strided), so it needs somewhere to land. clt_mdct_backward already + * destroys its input -- celt_synthesis copies the spectrum out first for + * exactly that reason -- and that buffer is N2 scalars, which is N4 complex, + * exactly the size wanted. So the common stride==1 case costs no memory at + * all. Short blocks arrive with stride>1 and a strided input that cannot be + * reused, but there N4 is only 60, so a 480-byte local covers it. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "kiss_fft.h" +#include "_kiss_fft_guts.h" +#include "mathops.h" +#include "stack_alloc.h" +#include "pfa.h" + +#ifdef OPUS_PFA + +/* The same constants the mixed-radix butterflies use, so the arithmetic + below is the arithmetic that was already shipping, only reassociated. + epi3 is -sin(2pi/3); ya/yb are the radix-5 sines; yc is sqrt(5)/4, which + collapses the four radix-5 cosine products into one multiply because + cos(2pi/5)+cos(4pi/5) is exactly -1/2 and the Q15 constants honour that + identity exactly (10126 - 26510 == -16384). */ +#define PFA_EPI3 (-28378) +#define PFA_YAI (-31164) +#define PFA_YBI (-19261) +#define PFA_YC ( 18318) + +/* PFA_KEEP_C keeps the reference compiled alongside an override so a + harness can score one against the other on target. */ +#if !defined(OVERRIDE_PFA_FFT15) || defined(PFA_KEEP_C) +/* One 15-point DFT, itself Good-Thomas over 3 and 5 so it too carries no + twiddles: five 3-point DFTs, then three 5-point DFTs. 40 real multiplies. + Input is five contiguous triples, laid out by the gather table. Output is + scattered by ostride complex elements; within one 5-point DFT the + destinations run at a stride of 3, which is what lets the kernel walk them + with a single post-indexed store. */ +void pfa_fft15_c(const kiss_fft_cpx *in, kiss_fft_cpx *out, int ostride) +{ + kiss_fft_cpx v[15]; + int g, k1; + + /* Five 3-point DFTs. Results land strided by 5 so each 5-point DFT + below reads a contiguous run. */ + for (g=0;g<5;g++) + { + kiss_fft_cpx x0, s3, s0, xm; + x0 = in[3*g]; + s3.r = ADD32_ovflw(in[3*g+1].r, in[3*g+2].r); + s3.i = ADD32_ovflw(in[3*g+1].i, in[3*g+2].i); + s0.r = SUB32_ovflw(in[3*g+1].r, in[3*g+2].r); + s0.i = SUB32_ovflw(in[3*g+1].i, in[3*g+2].i); + xm.r = SUB32_ovflw(x0.r, HALF_OF(s3.r)); + xm.i = SUB32_ovflw(x0.i, HALF_OF(s3.i)); + s0.r = S_MUL(s0.r, PFA_EPI3); + s0.i = S_MUL(s0.i, PFA_EPI3); + v[g].r = ADD32_ovflw(x0.r, s3.r); + v[g].i = ADD32_ovflw(x0.i, s3.i); + v[5+g].r = SUB32_ovflw(xm.r, s0.i); + v[5+g].i = ADD32_ovflw(xm.i, s0.r); + v[10+g].r = ADD32_ovflw(xm.r, s0.i); + v[10+g].i = SUB32_ovflw(xm.i, s0.r); + } + + /* Three 5-point DFTs. Output k2 of DFT k1 belongs at 15-point index + j = (10*k1 + 6*k2) mod 15; walking j from k1 in steps of 3 visits them + in the order k2 = (k1 + 3*t) mod 5, which is the permutation applied + when storing. */ + for (k1=0;k1<3;k1++) + { + const kiss_fft_cpx *x = v + 5*k1; + kiss_fft_cpx X[5], s7, s8, s9, s10, s3, s4, s5, s11, s6, s12; + int t; + + s7.r = ADD32_ovflw(x[1].r, x[4].r); s7.i = ADD32_ovflw(x[1].i, x[4].i); + s10.r = SUB32_ovflw(x[1].r, x[4].r); s10.i = SUB32_ovflw(x[1].i, x[4].i); + s8.r = ADD32_ovflw(x[2].r, x[3].r); s8.i = ADD32_ovflw(x[2].i, x[3].i); + s9.r = SUB32_ovflw(x[2].r, x[3].r); s9.i = SUB32_ovflw(x[2].i, x[3].i); + + s3.r = ADD32_ovflw(s7.r, s8.r); s3.i = ADD32_ovflw(s7.i, s8.i); + s4.r = SUB32_ovflw(s7.r, s8.r); s4.i = SUB32_ovflw(s7.i, s8.i); + + X[0].r = ADD32_ovflw(x[0].r, s3.r); + X[0].i = ADD32_ovflw(x[0].i, s3.i); + + s3.r = SUB32_ovflw(x[0].r, QUARTER_OF(s3.r)); + s3.i = SUB32_ovflw(x[0].i, QUARTER_OF(s3.i)); + s4.r = S_MUL(s4.r, PFA_YC); + s4.i = S_MUL(s4.i, PFA_YC); + + s5.r = ADD32_ovflw(s3.r, s4.r); s5.i = ADD32_ovflw(s3.i, s4.i); + s11.r = SUB32_ovflw(s3.r, s4.r); s11.i = SUB32_ovflw(s3.i, s4.i); + + s6.r = ADD32_ovflw(S_MUL(s10.i, PFA_YAI), S_MUL(s9.i, PFA_YBI)); + s6.i = NEG32_ovflw(ADD32_ovflw(S_MUL(s10.r, PFA_YAI), S_MUL(s9.r, PFA_YBI))); + X[1].r = SUB32_ovflw(s5.r, s6.r); X[1].i = SUB32_ovflw(s5.i, s6.i); + X[4].r = ADD32_ovflw(s5.r, s6.r); X[4].i = ADD32_ovflw(s5.i, s6.i); + + s12.r = SUB32_ovflw(S_MUL(s9.i, PFA_YAI), S_MUL(s10.i, PFA_YBI)); + s12.i = SUB32_ovflw(S_MUL(s10.r, PFA_YBI), S_MUL(s9.r, PFA_YAI)); + X[2].r = ADD32_ovflw(s11.r, s12.r); X[2].i = ADD32_ovflw(s11.i, s12.i); + X[3].r = SUB32_ovflw(s11.r, s12.r); X[3].i = SUB32_ovflw(s11.i, s12.i); + + for (t=0;t<5;t++) + out[(k1 + 3*t)*ostride] = X[(k1 + 3*t) % 5]; + } +} +#endif /* !OVERRIDE_PFA_FFT15 || PFA_KEEP_C */ + +#endif /* OPUS_PFA */ diff --git a/lib/rbcodec/codecs/libopus/celt/pfa.h b/lib/rbcodec/codecs/libopus/celt/pfa.h new file mode 100644 index 0000000000..bc932d4bb5 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/pfa.h @@ -0,0 +1,43 @@ +/* Good-Thomas (prime factor) FFT for the CELT backward MDCT. + See pfa.c for what it is and why. + + Copyright (c) 2026 Michael Giacomelli + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the conditions stated in + celt/kiss_fft.c are met. +*/ +#ifndef PFA_H +#define PFA_H + +#include "kiss_fft.h" + +#ifdef OPUS_PFA + +void pfa_fft15_c(const kiss_fft_cpx *in, kiss_fft_cpx *out, int ostride); + +#if defined(OPUS_ARM_ASM) && !defined(OPUS_ARM_NO_PFA_ASM) +# include "arm/pfa_arm.h" +#endif + +#ifndef OVERRIDE_PFA_FFT15 +# define PFA_FFT15(in, out, ostride) pfa_fft15_c(in, out, ostride) +#endif + +/* Pass 1 and pass 2. fin holds the gathered spectrum and fout receives the + transform; they must be different buffers, because pass 1 reads fifteen + contiguous points and writes them strided. nfft is 60, 120, 240 or 480. + + The result is left in Good-Thomas order: X[k] sits at + fout[(nfft/15)*(k % 15) + (k % (nfft/15))]. The post-rotation reads it + through that index, which costs two running counters and no table. */ +void opus_pfa_impl(const kiss_fft_cpx *fin, kiss_fft_cpx *fout, int nfft); + +/* True for the lengths opus_pfa_impl handles. Every 48 kHz CELT transform + qualifies; the test exists so a custom mode falls back to kiss_fft. */ +#define OPUS_PFA_SIZE(n) \ + ((n) == 60 || (n) == 120 || (n) == 240 || (n) == 480) + +#endif /* OPUS_PFA */ + +#endif /* PFA_H */ diff --git a/lib/rbcodec/codecs/libopus/celt/pfa_tables.h b/lib/rbcodec/codecs/libopus/celt/pfa_tables.h new file mode 100644 index 0000000000..73f309d370 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/pfa_tables.h @@ -0,0 +1,281 @@ +/* Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/kiss_fft.c are met. + */ +/* Good-Thomas index tables for the CELT backward MDCT. + + Generated by an offline script. Do not edit. + + N = 15*M with M a power of two, so 15 and M are co-prime and the prime + factor algorithm applies: the 15-point and M-point passes need no twiddle + factors between them at all, which is where the multiply saving comes + from. All that is left is the index algebra, and all of it is either a + table the pre-rotation was already reading or a running counter. +*/ +#ifndef PFA_TABLES_H +#define PFA_TABLES_H + +#include "opus_types.h" +#include "arch.h" + +/* n -> 15*n2 + 3*m2 + m1, for N=60, M=4. */ +static const opus_int16 pfa_in_60[60] ICONST_ATTR = { + 0, 56, 34, 27, 8, 46, 39, 20, 13, 51, 32, 25, + 3, 59, 37, 15, 11, 49, 42, 23, 1, 54, 35, 28, + 6, 47, 40, 18, 14, 52, 30, 26, 4, 57, 38, 16, + 9, 50, 43, 21, 2, 55, 33, 29, 7, 45, 41, 19, + 12, 53, 31, 24, 5, 58, 36, 17, 10, 48, 44, 22, +}; + +/* n -> 15*n2 + 3*m2 + m1, for N=120, M=8. */ +static const opus_int16 pfa_in_120[120] ICONST_ATTR = { + 0, 118, 101, 81, 64, 47, 42, 25, 8, 108, 91, 89, + 69, 52, 35, 15, 13, 116, 96, 79, 62, 57, 40, 23, + 3, 106, 104, 84, 67, 50, 30, 28, 11, 111, 94, 77, + 72, 55, 38, 18, 1, 119, 99, 82, 65, 45, 43, 26, + 6, 109, 92, 87, 70, 53, 33, 16, 14, 114, 97, 80, + 60, 58, 41, 21, 4, 107, 102, 85, 68, 48, 31, 29, + 9, 112, 95, 75, 73, 56, 36, 19, 2, 117, 100, 83, + 63, 46, 44, 24, 7, 110, 90, 88, 71, 51, 34, 17, + 12, 115, 98, 78, 61, 59, 39, 22, 5, 105, 103, 86, + 66, 49, 32, 27, 10, 113, 93, 76, 74, 54, 37, 20, +}; + +/* n -> 15*n2 + 3*m2 + m1, for N=240, M=16. */ +static const opus_int16 pfa_in_240[240] ICONST_ATTR = { + 0, 233, 223, 198, 191, 166, 156, 149, 124, 114, 92, 82, + 72, 50, 40, 15, 8, 238, 213, 206, 181, 171, 164, 139, + 129, 107, 97, 87, 65, 55, 30, 23, 13, 228, 221, 196, + 186, 179, 154, 144, 122, 112, 102, 80, 70, 45, 38, 28, + 3, 236, 211, 201, 194, 169, 159, 137, 127, 117, 95, 85, + 60, 53, 43, 18, 11, 226, 216, 209, 184, 174, 152, 142, + 132, 110, 100, 75, 68, 58, 33, 26, 1, 231, 224, 199, + 189, 167, 157, 147, 125, 115, 90, 83, 73, 48, 41, 16, + 6, 239, 214, 204, 182, 172, 162, 140, 130, 105, 98, 88, + 63, 56, 31, 21, 14, 229, 219, 197, 187, 177, 155, 145, + 120, 113, 103, 78, 71, 46, 36, 29, 4, 234, 212, 202, + 192, 170, 160, 135, 128, 118, 93, 86, 61, 51, 44, 19, + 9, 227, 217, 207, 185, 175, 150, 143, 133, 108, 101, 76, + 66, 59, 34, 24, 2, 232, 222, 200, 190, 165, 158, 148, + 123, 116, 91, 81, 74, 49, 39, 17, 7, 237, 215, 205, + 180, 173, 163, 138, 131, 106, 96, 89, 64, 54, 32, 22, + 12, 230, 220, 195, 188, 178, 153, 146, 121, 111, 104, 79, + 69, 47, 37, 27, 5, 235, 210, 203, 193, 168, 161, 136, + 126, 119, 94, 84, 62, 52, 42, 20, 10, 225, 218, 208, + 183, 176, 151, 141, 134, 109, 99, 77, 67, 57, 35, 25, +}; + +/* n -> 15*n2 + 3*m2 + m1, for N=480, M=32. */ +static const opus_int16 pfa_in_480[480] ICONST_ATTR = { + 0, 229, 458, 204, 433, 167, 393, 142, 371, 117, 331, 80, + 306, 55, 284, 15, 244, 473, 219, 448, 182, 408, 157, 386, + 132, 346, 95, 321, 70, 299, 30, 259, 8, 234, 463, 197, + 423, 172, 401, 147, 361, 110, 336, 85, 314, 45, 274, 23, + 249, 478, 212, 438, 187, 416, 162, 376, 125, 351, 100, 329, + 60, 289, 38, 264, 13, 227, 453, 202, 431, 177, 391, 140, + 366, 115, 344, 75, 304, 53, 279, 28, 242, 468, 217, 446, + 192, 406, 155, 381, 130, 359, 90, 319, 68, 294, 43, 257, + 3, 232, 461, 207, 421, 170, 396, 145, 374, 105, 334, 83, + 309, 58, 272, 18, 247, 476, 222, 436, 185, 411, 160, 389, + 120, 349, 98, 324, 73, 287, 33, 262, 11, 237, 451, 200, + 426, 175, 404, 135, 364, 113, 339, 88, 302, 48, 277, 26, + 252, 466, 215, 441, 190, 419, 150, 379, 128, 354, 103, 317, + 63, 292, 41, 267, 1, 230, 456, 205, 434, 165, 394, 143, + 369, 118, 332, 78, 307, 56, 282, 16, 245, 471, 220, 449, + 180, 409, 158, 384, 133, 347, 93, 322, 71, 297, 31, 260, + 6, 235, 464, 195, 424, 173, 399, 148, 362, 108, 337, 86, + 312, 46, 275, 21, 250, 479, 210, 439, 188, 414, 163, 377, + 123, 352, 101, 327, 61, 290, 36, 265, 14, 225, 454, 203, + 429, 178, 392, 138, 367, 116, 342, 76, 305, 51, 280, 29, + 240, 469, 218, 444, 193, 407, 153, 382, 131, 357, 91, 320, + 66, 295, 44, 255, 4, 233, 459, 208, 422, 168, 397, 146, + 372, 106, 335, 81, 310, 59, 270, 19, 248, 474, 223, 437, + 183, 412, 161, 387, 121, 350, 96, 325, 74, 285, 34, 263, + 9, 238, 452, 198, 427, 176, 402, 136, 365, 111, 340, 89, + 300, 49, 278, 24, 253, 467, 213, 442, 191, 417, 151, 380, + 126, 355, 104, 315, 64, 293, 39, 268, 2, 228, 457, 206, + 432, 166, 395, 141, 370, 119, 330, 79, 308, 54, 283, 17, + 243, 472, 221, 447, 181, 410, 156, 385, 134, 345, 94, 323, + 69, 298, 32, 258, 7, 236, 462, 196, 425, 171, 400, 149, + 360, 109, 338, 84, 313, 47, 273, 22, 251, 477, 211, 440, + 186, 415, 164, 375, 124, 353, 99, 328, 62, 288, 37, 266, + 12, 226, 455, 201, 430, 179, 390, 139, 368, 114, 343, 77, + 303, 52, 281, 27, 241, 470, 216, 445, 194, 405, 154, 383, + 129, 358, 92, 318, 67, 296, 42, 256, 5, 231, 460, 209, + 420, 169, 398, 144, 373, 107, 333, 82, 311, 57, 271, 20, + 246, 475, 224, 435, 184, 413, 159, 388, 122, 348, 97, 326, + 72, 286, 35, 261, 10, 239, 450, 199, 428, 174, 403, 137, + 363, 112, 341, 87, 301, 50, 276, 25, 254, 465, 214, 443, + 189, 418, 152, 378, 127, 356, 102, 316, 65, 291, 40, 269, +}; + +/* Bit-reversal of the M-point sub-FFT, folded into the 15-point + kernel's output scatter so the radix-4 chain sees natural order. */ +static const opus_int16 pfa_brev_4[4] ICONST_ATTR = { + 0, 1, 2, 3, +}; + +static const opus_int16 pfa_brev_8[8] ICONST_ATTR = { + 0, 4, 1, 5, 2, 6, 3, 7, +}; + +static const opus_int16 pfa_brev_16[16] ICONST_ATTR = { + 0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, +}; + +static const opus_int16 pfa_brev_32[32] ICONST_ATTR = { + 0, 8, 16, 24, 4, 12, 20, 28, 1, 9, 17, 25, 5, 13, 21, 29, + 2, 10, 18, 26, 6, 14, 22, 30, 3, 11, 19, 27, 7, 15, 23, 31, +}; + +/* Where the post-rotation finds X[k]. The transform is left in + Good-Thomas order, X[k] at M*(k mod 15) + (k mod M). Both residues are + running counters, but keeping four of them plus their wrap constants + costs more registers than the kernel has to spare, so the composed index + is tabulated instead, pre-scaled to a byte offset. The post-rotation + walks it forwards for the ascending end and backwards for the descending + one, so a single table serves both. */ +static const opus_int16 pfa_post_60[60] ICONST_ATTR = { + 0, 40, 80, 120, 128, 168, 208, 248, 256, 296, + 336, 376, 384, 424, 464, 24, 32, 72, 112, 152, + 160, 200, 240, 280, 288, 328, 368, 408, 416, 456, + 16, 56, 64, 104, 144, 184, 192, 232, 272, 312, + 320, 360, 400, 440, 448, 8, 48, 88, 96, 136, + 176, 216, 224, 264, 304, 344, 352, 392, 432, 472, +}; + +static const opus_int16 pfa_post_120[120] ICONST_ATTR = { + 0, 72, 144, 216, 288, 360, 432, 504, 512, 584, + 656, 728, 800, 872, 944, 56, 64, 136, 208, 280, + 352, 424, 496, 568, 576, 648, 720, 792, 864, 936, + 48, 120, 128, 200, 272, 344, 416, 488, 560, 632, + 640, 712, 784, 856, 928, 40, 112, 184, 192, 264, + 336, 408, 480, 552, 624, 696, 704, 776, 848, 920, + 32, 104, 176, 248, 256, 328, 400, 472, 544, 616, + 688, 760, 768, 840, 912, 24, 96, 168, 240, 312, + 320, 392, 464, 536, 608, 680, 752, 824, 832, 904, + 16, 88, 160, 232, 304, 376, 384, 456, 528, 600, + 672, 744, 816, 888, 896, 8, 80, 152, 224, 296, + 368, 440, 448, 520, 592, 664, 736, 808, 880, 952, +}; + +static const opus_int16 pfa_post_240[240] ICONST_ATTR = { + 0, 136, 272, 408, 544, 680, 816, 952, 1088, 1224, + 1360, 1496, 1632, 1768, 1904, 120, 128, 264, 400, 536, + 672, 808, 944, 1080, 1216, 1352, 1488, 1624, 1760, 1896, + 112, 248, 256, 392, 528, 664, 800, 936, 1072, 1208, + 1344, 1480, 1616, 1752, 1888, 104, 240, 376, 384, 520, + 656, 792, 928, 1064, 1200, 1336, 1472, 1608, 1744, 1880, + 96, 232, 368, 504, 512, 648, 784, 920, 1056, 1192, + 1328, 1464, 1600, 1736, 1872, 88, 224, 360, 496, 632, + 640, 776, 912, 1048, 1184, 1320, 1456, 1592, 1728, 1864, + 80, 216, 352, 488, 624, 760, 768, 904, 1040, 1176, + 1312, 1448, 1584, 1720, 1856, 72, 208, 344, 480, 616, + 752, 888, 896, 1032, 1168, 1304, 1440, 1576, 1712, 1848, + 64, 200, 336, 472, 608, 744, 880, 1016, 1024, 1160, + 1296, 1432, 1568, 1704, 1840, 56, 192, 328, 464, 600, + 736, 872, 1008, 1144, 1152, 1288, 1424, 1560, 1696, 1832, + 48, 184, 320, 456, 592, 728, 864, 1000, 1136, 1272, + 1280, 1416, 1552, 1688, 1824, 40, 176, 312, 448, 584, + 720, 856, 992, 1128, 1264, 1400, 1408, 1544, 1680, 1816, + 32, 168, 304, 440, 576, 712, 848, 984, 1120, 1256, + 1392, 1528, 1536, 1672, 1808, 24, 160, 296, 432, 568, + 704, 840, 976, 1112, 1248, 1384, 1520, 1656, 1664, 1800, + 16, 152, 288, 424, 560, 696, 832, 968, 1104, 1240, + 1376, 1512, 1648, 1784, 1792, 8, 144, 280, 416, 552, + 688, 824, 960, 1096, 1232, 1368, 1504, 1640, 1776, 1912, +}; + +static const opus_int16 pfa_post_480[480] ICONST_ATTR = { + 0, 264, 528, 792, 1056, 1320, 1584, 1848, 2112, 2376, + 2640, 2904, 3168, 3432, 3696, 120, 384, 648, 912, 1176, + 1440, 1704, 1968, 2232, 2496, 2760, 3024, 3288, 3552, 3816, + 240, 504, 512, 776, 1040, 1304, 1568, 1832, 2096, 2360, + 2624, 2888, 3152, 3416, 3680, 104, 368, 632, 896, 1160, + 1424, 1688, 1952, 2216, 2480, 2744, 3008, 3272, 3536, 3800, + 224, 488, 752, 1016, 1024, 1288, 1552, 1816, 2080, 2344, + 2608, 2872, 3136, 3400, 3664, 88, 352, 616, 880, 1144, + 1408, 1672, 1936, 2200, 2464, 2728, 2992, 3256, 3520, 3784, + 208, 472, 736, 1000, 1264, 1528, 1536, 1800, 2064, 2328, + 2592, 2856, 3120, 3384, 3648, 72, 336, 600, 864, 1128, + 1392, 1656, 1920, 2184, 2448, 2712, 2976, 3240, 3504, 3768, + 192, 456, 720, 984, 1248, 1512, 1776, 2040, 2048, 2312, + 2576, 2840, 3104, 3368, 3632, 56, 320, 584, 848, 1112, + 1376, 1640, 1904, 2168, 2432, 2696, 2960, 3224, 3488, 3752, + 176, 440, 704, 968, 1232, 1496, 1760, 2024, 2288, 2552, + 2560, 2824, 3088, 3352, 3616, 40, 304, 568, 832, 1096, + 1360, 1624, 1888, 2152, 2416, 2680, 2944, 3208, 3472, 3736, + 160, 424, 688, 952, 1216, 1480, 1744, 2008, 2272, 2536, + 2800, 3064, 3072, 3336, 3600, 24, 288, 552, 816, 1080, + 1344, 1608, 1872, 2136, 2400, 2664, 2928, 3192, 3456, 3720, + 144, 408, 672, 936, 1200, 1464, 1728, 1992, 2256, 2520, + 2784, 3048, 3312, 3576, 3584, 8, 272, 536, 800, 1064, + 1328, 1592, 1856, 2120, 2384, 2648, 2912, 3176, 3440, 3704, + 128, 392, 656, 920, 1184, 1448, 1712, 1976, 2240, 2504, + 2768, 3032, 3296, 3560, 3824, 248, 256, 520, 784, 1048, + 1312, 1576, 1840, 2104, 2368, 2632, 2896, 3160, 3424, 3688, + 112, 376, 640, 904, 1168, 1432, 1696, 1960, 2224, 2488, + 2752, 3016, 3280, 3544, 3808, 232, 496, 760, 768, 1032, + 1296, 1560, 1824, 2088, 2352, 2616, 2880, 3144, 3408, 3672, + 96, 360, 624, 888, 1152, 1416, 1680, 1944, 2208, 2472, + 2736, 3000, 3264, 3528, 3792, 216, 480, 744, 1008, 1272, + 1280, 1544, 1808, 2072, 2336, 2600, 2864, 3128, 3392, 3656, + 80, 344, 608, 872, 1136, 1400, 1664, 1928, 2192, 2456, + 2720, 2984, 3248, 3512, 3776, 200, 464, 728, 992, 1256, + 1520, 1784, 1792, 2056, 2320, 2584, 2848, 3112, 3376, 3640, + 64, 328, 592, 856, 1120, 1384, 1648, 1912, 2176, 2440, + 2704, 2968, 3232, 3496, 3760, 184, 448, 712, 976, 1240, + 1504, 1768, 2032, 2296, 2304, 2568, 2832, 3096, 3360, 3624, + 48, 312, 576, 840, 1104, 1368, 1632, 1896, 2160, 2424, + 2688, 2952, 3216, 3480, 3744, 168, 432, 696, 960, 1224, + 1488, 1752, 2016, 2280, 2544, 2808, 2816, 3080, 3344, 3608, + 32, 296, 560, 824, 1088, 1352, 1616, 1880, 2144, 2408, + 2672, 2936, 3200, 3464, 3728, 152, 416, 680, 944, 1208, + 1472, 1736, 2000, 2264, 2528, 2792, 3056, 3320, 3328, 3592, + 16, 280, 544, 808, 1072, 1336, 1600, 1864, 2128, 2392, + 2656, 2920, 3184, 3448, 3712, 136, 400, 664, 928, 1192, + 1456, 1720, 1984, 2248, 2512, 2776, 3040, 3304, 3568, 3832, +}; + +/* Twiddles for the M-point half. One 32-entry table serves every size: + M=16 reads every second entry, M=8 every fourth, M=4 every eighth, which + is exactly the shift mechanism kiss_fft already uses to share one table + between the four transform lengths. */ +static const kiss_twiddle_cpx pfa_tw32[32] ICONST_ATTR = { + { 32767, 0}, + { 32137, -6393}, + { 30273, -12539}, + { 27245, -18204}, + { 23170, -23170}, + { 18204, -27245}, + { 12539, -30273}, + { 6393, -32137}, + { 0, -32767}, + { -6393, -32137}, + {-12539, -30273}, + {-18204, -27245}, + {-23170, -23170}, + {-27245, -18204}, + {-30273, -12539}, + {-32137, -6393}, + {-32767, 0}, + {-32137, 6393}, + {-30273, 12539}, + {-27245, 18204}, + {-23170, 23170}, + {-18204, 27245}, + {-12539, 30273}, + { -6393, 32137}, + { 0, 32767}, + { 6393, 32137}, + { 12539, 30273}, + { 18204, 27245}, + { 23170, 23170}, + { 27245, 18204}, + { 30273, 12539}, + { 32137, 6393}, +}; + +#endif /* PFA_TABLES_H */ diff --git a/lib/rbcodec/codecs/libopus/config.h b/lib/rbcodec/codecs/libopus/config.h index 418ccfd6d2..d7c39fd2ca 100644 --- a/lib/rbcodec/codecs/libopus/config.h +++ b/lib/rbcodec/codecs/libopus/config.h @@ -82,6 +82,14 @@ #endif +/* Good-Thomas FFT for the backward MDCT. Every 48 kHz CELT transform length + is 15 times a power of two, so the prime factor algorithm applies and the + inter-stage twiddles -- 73% of the FFT multiplies -- disappear. Define + OPUS_NO_PFA to fall back to the mixed-radix chain. */ +#ifndef OPUS_NO_PFA +#define OPUS_PFA +#endif + #if defined(CPU_COLDFIRE) #define OPUS_CF_INLINE_ASM #endif From 40fdf6ac110f9828183c32bbefe34efe0756c1ee Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sat, 26 Sep 2026 14:26:48 -0400 Subject: [PATCH 236/408] libopus: Fix yellow and red in da9df96c30 * simulator warnings * non-arm warnings * armv7-m errors * armv7-a errors Change-Id: I285ab466c07170e632f1bec488eb471bcbd4a566 --- lib/rbcodec/codecs/libopus/SOURCES | 2 +- lib/rbcodec/codecs/libopus/celt/mdct.c | 11 +++++++---- lib/rbcodec/codecs/libopus/config.h | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index 53fb4c24be..63c04f41bf 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -15,7 +15,7 @@ celt/arm/pfa_armv4_asm.S celt/arm/comb_filter_armv4_asm.S celt/arm/denorm_armv4_asm.S celt/arm/deemph_armv4_asm.S -#elif defined(CPU_ARM) && (ARM_ARCH >= 5) && (ARCH_PROFILE == ARM_PROFILE_CLASSIC) +#elif defined(CPU_ARM) && (ARM_ARCH >= 5) && (ARCH_PROFILE != ARM_PROFILE_MICRO) celt/arm/kiss_fft_armv5e_asm.S celt/arm/mdct_armv5e_asm.S celt/arm/pfa_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/celt/mdct.c b/lib/rbcodec/codecs/libopus/celt/mdct.c index eabc9ae615..b630c69b10 100644 --- a/lib/rbcodec/codecs/libopus/celt/mdct.c +++ b/lib/rbcodec/codecs/libopus/celt/mdct.c @@ -55,6 +55,11 @@ #include "pfa.h" #include "pfa_tables.h" +#if defined(OPUS_ARM_ASM) +#include "arm/mdct_armv4.h" +#include "arm/mdct_armv5e.h" +#endif + /* The pre-rotation already scatters through a table; for the prime factor transform it reads this one instead of the bit-reversal, which is why the gather costs nothing. */ @@ -69,6 +74,7 @@ static const opus_int16 *pfa_gather(int n) } } +#ifdef OVERRIDE_MDCT_POSTROT_PFA /* Where the post-rotation finds X[k], as a byte offset. */ static const opus_int16 *pfa_postmap(int n) { @@ -80,16 +86,13 @@ static const opus_int16 *pfa_postmap(int n) default: return pfa_post_480; } } +#endif /* Walk p(k) = M*(k mod 15) + (k mod M) by one step. Both residues are running counters, so the post-rotation needs no index table. */ #define PFA_ADV_UP(p, k1, k2, M) do { (p) += (M)+1; if (++(k1) == 15) { (k1) = 0; (p) -= 15*(M); } if (++(k2) == (M)) { (k2) = 0; (p) -= (M); } } while (0) #define PFA_ADV_DN(p, k1, k2, M) do { (p) -= (M)+1; if ((k1)-- == 0) { (k1) = 14; (p) += 15*(M); } if ((k2)-- == 0) { (k2) = (M)-1; (p) += (M); } } while (0) #endif -#if defined(OPUS_ARM_ASM) -#include "arm/mdct_armv4.h" -#include "arm/mdct_armv5e.h" -#endif #include "stack_alloc.h" #if defined(MIPSr1_ASM) diff --git a/lib/rbcodec/codecs/libopus/config.h b/lib/rbcodec/codecs/libopus/config.h index d7c39fd2ca..72cfb45d0e 100644 --- a/lib/rbcodec/codecs/libopus/config.h +++ b/lib/rbcodec/codecs/libopus/config.h @@ -61,12 +61,13 @@ #define OPUS_ARM_INLINE_ASM #elif ARM_ARCH > 4 #define OPUS_ARM_INLINE_EDSP -#if (ARCH_PROFILE != ARM_PROFILE_CLASSIC) +#if (ARCH_PROFILE == ARM_PROFILE_MICRO) #define OPUS_ARM_NO_FFT_ASM #define OPUS_ARM_NO_MDCT_ASM #define OPUS_ARM_NO_PITCH_ASM +#define OPUS_NO_PFA #endif -#endif +#endif /* ARM_ARCH */ /*optimization no hardware division support*/ #if !defined(ARM_HAVE_HW_DIV) @@ -79,7 +80,6 @@ #if CONFIG_CPU == PP5022 || CONFIG_CPU == PP5024 #define OPUS_LOG2TAN_TABLE #endif - #endif /* Good-Thomas FFT for the backward MDCT. Every 48 kHz CELT transform length From a51adaac4fcd1497923703f48787c7043dc0c7cd Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:29 -0400 Subject: [PATCH 237/408] opus: place the hot decode path in IRAM on PP5022/5024 34 functions and the ARMv4 kernels, chosen by a greedy fill of the free codec IRAM window ranked by cycles per byte. The mixed-radix butterflies give their IRAM back, being unreachable under the prime factor transform. The cycle model does not see this at all: it models no instruction cache. Measured: e200v1 48.96 -> 42.60 MHz; reclaiming the mixed-radix IRAM was a further -0.52%. Co-Authored-By: Claude Opus 5 Change-Id: I9e22025cba99521750a3b664cd0d59357fcd0810 --- lib/rbcodec/codecs/libopus/celt/arch.h | 13 ++++++++ .../libopus/celt/arm/comb_filter_armv4_asm.S | 4 +++ .../libopus/celt/arm/deemph_armv4_asm.S | 4 +++ .../libopus/celt/arm/denorm_armv4_asm.S | 4 +++ .../libopus/celt/arm/kiss_fft_armv4_asm.S | 26 ++++++++++++++++ .../codecs/libopus/celt/arm/mdct_armv4_asm.S | 16 ++++++++++ .../codecs/libopus/celt/arm/pfa_armv4_asm.S | 4 +++ lib/rbcodec/codecs/libopus/celt/bands.c | 20 ++++++------ lib/rbcodec/codecs/libopus/celt/celt.c | 2 +- .../codecs/libopus/celt/celt_decoder.c | 4 +-- lib/rbcodec/codecs/libopus/celt/cwrs.c | 2 +- lib/rbcodec/codecs/libopus/celt/entcode.c | 2 +- lib/rbcodec/codecs/libopus/celt/entdec.c | 14 ++++----- lib/rbcodec/codecs/libopus/celt/kiss_fft.c | 4 +-- lib/rbcodec/codecs/libopus/celt/laplace.c | 2 +- lib/rbcodec/codecs/libopus/celt/mathops.c | 4 +-- lib/rbcodec/codecs/libopus/celt/quant_bands.c | 4 +-- lib/rbcodec/codecs/libopus/celt/rate.c | 2 +- .../codecs/libopus/celt/static_modes_fixed.h | 4 ++- lib/rbcodec/codecs/libopus/celt/vq.c | 10 +++--- lib/rbcodec/codecs/libopus/config.h | 31 +++++++++++++++++++ 21 files changed, 140 insertions(+), 36 deletions(-) diff --git a/lib/rbcodec/codecs/libopus/celt/arch.h b/lib/rbcodec/codecs/libopus/celt/arch.h index 6437f59624..39f01db5e5 100644 --- a/lib/rbcodec/codecs/libopus/celt/arch.h +++ b/lib/rbcodec/codecs/libopus/celt/arch.h @@ -37,6 +37,19 @@ #include "opus_types.h" #include "opus_defines.h" +/* Placement of the hot decode path in fast on-chip memory. Rockbox config.h + sets this to ICODE_ATTR on the targets whose codec IRAM window has room; + everywhere else, and in the standalone test harness, it is nothing. */ +#ifndef ICODE_ATTR_OPUS +# define ICODE_ATTR_OPUS +#endif +#ifndef ICODE_ATTR_OPUS_MR +# define ICODE_ATTR_OPUS_MR +#endif +#ifndef ICONST_ATTR_OPUS_MR +# define ICONST_ATTR_OPUS_MR ICONST_ATTR +#endif + # if !defined(__GNUC_PREREQ) # if defined(__GNUC__)&&defined(__GNUC_MINOR__) # define __GNUC_PREREQ(_maj,_min) \ diff --git a/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_armv4_asm.S index eb27bc32a4..53e7b74d82 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_armv4_asm.S +++ b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_armv4_asm.S @@ -61,7 +61,11 @@ #include "config.h" #endif +#ifdef OPUS_ARM_ICODE + .section .icode, "ax", %progbits +#else .text +#endif .align 2 /* One output sample. diff --git a/lib/rbcodec/codecs/libopus/celt/arm/deemph_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/deemph_armv4_asm.S index 8336c7183c..f81ba729e8 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/deemph_armv4_asm.S +++ b/lib/rbcodec/codecs/libopus/celt/arm/deemph_armv4_asm.S @@ -41,7 +41,11 @@ #include "config.h" #endif +#ifdef OPUS_ARM_ICODE + .section .icode, "ax", %progbits +#else .text +#endif .align 2 .macro PCM16 src, tmp diff --git a/lib/rbcodec/codecs/libopus/celt/arm/denorm_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/denorm_armv4_asm.S index 7e5ad8ac46..93c4e87374 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/denorm_armv4_asm.S +++ b/lib/rbcodec/codecs/libopus/celt/arm/denorm_armv4_asm.S @@ -40,7 +40,11 @@ #include "config.h" #endif +#ifdef OPUS_ARM_ICODE + .section .icode, "ax", %progbits +#else .text +#endif .align 2 /* ------------------------------------------------------------------------ diff --git a/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv4_asm.S index 50a037271a..0f61657e52 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv4_asm.S +++ b/lib/rbcodec/codecs/libopus/celt/arm/kiss_fft_armv4_asm.S @@ -42,7 +42,21 @@ #error "kiss_fft_armv4_asm.S must be assembled in ARM mode" #endif +/* These kernels are 2,892 bytes that carry 43% of the cycles an ARMv4 decode + * spends, which makes them the densest thing in the codec to put in IRAM. + * config.h sets OPUS_ARM_ICODE on the targets whose codec IRAM window has + * room; everywhere else this stays in .text. The firmware config.h it reads + * that from is assembly-safe, and libopus/config.h guards the rest of its + * includes with __ASSEMBLER__ so this include costs nothing here. */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#ifdef OPUS_ARM_ICODE + .section .icode, "ax", %progbits +#else .text +#endif .align 2 /* ------------------------------------------------------------------------ @@ -241,6 +255,18 @@ kf_bfly4_armv4: * 28 mm*8 32 m*8 * ------------------------------------------------------------------------ */ .align 2 + +/* Unreachable once the prime factor transform is built, so it gives its IRAM + back rather than sitting resident and unread. Still assembled: a size the + Good-Thomas path does not cover falls through to it, and correct-but-slow + from DRAM is the right failure mode. */ +#if defined(OPUS_ARM_ICODE) && !defined(OPUS_PFA) + .section .icode, "ax", %progbits +#else + .text +#endif + .align 2 + .global kf_bfly3_armv4 .type kf_bfly3_armv4, %function kf_bfly3_armv4: diff --git a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4_asm.S index 7e264b2bf2..6115cb5ec3 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4_asm.S +++ b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4_asm.S @@ -51,7 +51,11 @@ #include "config.h" #endif +#ifdef OPUS_ARM_ICODE + .section .icode, "ax", %progbits +#else .text +#endif .align 2 /* One Q15 complex multiply, the same register discipline as the C_MUL asm in @@ -134,7 +138,11 @@ mdct_prerot_armv4: back rather than sitting resident and unread. Still assembled: a size the Good-Thomas path does not cover falls through to it, and correct-but-slow from DRAM is the right failure mode. */ +#if defined(OPUS_ARM_ICODE) && !defined(OPUS_PFA) + .section .icode, "ax", %progbits +#else .text +#endif .align 2 .global mdct_postrot_armv4 @@ -182,7 +190,11 @@ mdct_postrot_armv4: * ------------------------------------------------------------------------ */ .align 2 +#ifdef OPUS_ARM_ICODE + .section .icode, "ax", %progbits +#else .text +#endif .align 2 .global mdct_mirror_armv4 @@ -209,7 +221,11 @@ mdct_mirror_armv4: #ifdef OPUS_PFA +#ifdef OPUS_ARM_ICODE + .section .icode, "ax", %progbits +#else .text +#endif /* ------------------------------------------------------------------------ * void mdct_postrot_pfa_armv4(const kiss_fft_scalar *S, diff --git a/lib/rbcodec/codecs/libopus/celt/arm/pfa_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/pfa_armv4_asm.S index ce0b2ae9f0..7090cde931 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/pfa_armv4_asm.S +++ b/lib/rbcodec/codecs/libopus/celt/arm/pfa_armv4_asm.S @@ -46,7 +46,11 @@ #include "config.h" #endif +#ifdef OPUS_ARM_ICODE + .section .icode, "ax", %progbits +#else .text +#endif .align 2 /* Frame: v[15] complex at 0..119, the s5/s11 park at 120..135, then the diff --git a/lib/rbcodec/codecs/libopus/celt/bands.c b/lib/rbcodec/codecs/libopus/celt/bands.c index 9b21dfcaf4..3f95dd25af 100644 --- a/lib/rbcodec/codecs/libopus/celt/bands.c +++ b/lib/rbcodec/codecs/libopus/celt/bands.c @@ -92,7 +92,7 @@ opus_int16 bitexact_cos(opus_int16 x) return 1+x2; } -int bitexact_log2tan(int isin,int icos) +ICODE_ATTR_OPUS_MR int bitexact_log2tan(int isin,int icos) { int lc; int ls; @@ -208,7 +208,7 @@ void normalise_bands(const CELTMode *m, const celt_sig * OPUS_RESTRICT freq, cel #endif /* FIXED_POINT */ /* De-normalise the energy to produce the synthesis from the unit-energy bands */ -void denormalise_bands(const CELTMode *m, const celt_norm * OPUS_RESTRICT X, +ICODE_ATTR_OPUS void denormalise_bands(const CELTMode *m, const celt_norm * OPUS_RESTRICT X, celt_sig * OPUS_RESTRICT freq, const opus_val16 *bandLogE, int start, int end, int M, int downsample, int silence) { @@ -288,7 +288,7 @@ void denormalise_bands(const CELTMode *m, const celt_norm * OPUS_RESTRICT X, } /* This prevents energy collapse for transients with multiple short MDCTs */ -void anti_collapse(const CELTMode *m, celt_norm *X_, unsigned char *collapse_masks, int LM, int C, int size, +ICODE_ATTR_OPUS void anti_collapse(const CELTMode *m, celt_norm *X_, unsigned char *collapse_masks, int LM, int C, int size, int start, int end, const opus_val16 *logE, const opus_val16 *prev1logE, const opus_val16 *prev2logE, const int *pulses, opus_uint32 seed, int arch) { @@ -603,7 +603,7 @@ static const int ordery_table[] = { 15, 0, 8, 7, 12, 3, 11, 4, 14, 1, 9, 6, 13, 2, 10, 5, }; -static void deinterleave_hadamard(celt_norm *X, int N0, int stride, int hadamard) +ICODE_ATTR_OPUS static void deinterleave_hadamard(celt_norm *X, int N0, int stride, int hadamard) { int i,j; VARDECL(celt_norm, tmp); @@ -653,7 +653,7 @@ static void interleave_hadamard(celt_norm *X, int N0, int stride, int hadamard) } #ifndef OVERRIDE_haar1 -void haar1(celt_norm *X, int N0, int stride) +ICODE_ATTR_OPUS void haar1(celt_norm *X, int N0, int stride) { int i, j; N0 >>= 1; @@ -722,7 +722,7 @@ struct split_ctx { int qalloc; }; -static void compute_theta(struct band_ctx *ctx, struct split_ctx *sctx, +ICODE_ATTR_OPUS static void compute_theta(struct band_ctx *ctx, struct split_ctx *sctx, celt_norm *X, celt_norm *Y, int N, int *b, int B, int B0, int LM, int stereo, int *fill) @@ -996,7 +996,7 @@ static unsigned quant_band_n1(struct band_ctx *ctx, celt_norm *X, celt_norm *Y, It can split the band in two and transmit the energy difference with the two half-bands. It can be called recursively so bands can end up being split in 8 parts. */ -static unsigned quant_partition(struct band_ctx *ctx, celt_norm *X, +ICODE_ATTR_OPUS static unsigned quant_partition(struct band_ctx *ctx, celt_norm *X, int N, int b, int B, celt_norm *lowband, int LM, opus_val16 gain, int fill) @@ -1162,7 +1162,7 @@ static unsigned quant_partition(struct band_ctx *ctx, celt_norm *X, /* This function is responsible for encoding and decoding a band for the mono case. */ -static unsigned quant_band(struct band_ctx *ctx, celt_norm *X, +ICODE_ATTR_OPUS static unsigned quant_band(struct band_ctx *ctx, celt_norm *X, int N, int b, int B, celt_norm *lowband, int LM, celt_norm *lowband_out, opus_val16 gain, celt_norm *lowband_scratch, int fill) @@ -1288,7 +1288,7 @@ static unsigned quant_band(struct band_ctx *ctx, celt_norm *X, /* This function is responsible for encoding and decoding a band for the stereo case. */ -static unsigned quant_band_stereo(struct band_ctx *ctx, celt_norm *X, celt_norm *Y, +ICODE_ATTR_OPUS static unsigned quant_band_stereo(struct band_ctx *ctx, celt_norm *X, celt_norm *Y, int N, int b, int B, celt_norm *lowband, int LM, celt_norm *lowband_out, celt_norm *lowband_scratch, int fill) @@ -1449,7 +1449,7 @@ static void special_hybrid_folding(const CELTMode *m, celt_norm *norm, celt_norm OPUS_COPY(&norm2[n1], &norm2[2*n1 - n2], n2-n1); } -void quant_all_bands(int encode, const CELTMode *m, int start, int end, +ICODE_ATTR_OPUS void quant_all_bands(int encode, const CELTMode *m, int start, int end, celt_norm *X_, celt_norm *Y_, unsigned char *collapse_masks, const celt_ener *bandE, int *pulses, int shortBlocks, int spread, int dual_stereo, int intensity, int *tf_res, opus_int32 total_bits, diff --git a/lib/rbcodec/codecs/libopus/celt/celt.c b/lib/rbcodec/codecs/libopus/celt/celt.c index 9ce234695c..633e932cd5 100644 --- a/lib/rbcodec/codecs/libopus/celt/celt.c +++ b/lib/rbcodec/codecs/libopus/celt/celt.c @@ -187,7 +187,7 @@ void comb_filter_const_c(opus_val32 *y, opus_val32 *x, int T, int N, #endif #ifndef OVERRIDE_comb_filter -void comb_filter(opus_val32 *y, opus_val32 *x, int T0, int T1, int N, +ICODE_ATTR_OPUS void comb_filter(opus_val32 *y, opus_val32 *x, int T0, int T1, int N, opus_val16 g0, opus_val16 g1, int tapset0, int tapset1, const opus_val16 *window, int overlap, int arch) { diff --git a/lib/rbcodec/codecs/libopus/celt/celt_decoder.c b/lib/rbcodec/codecs/libopus/celt/celt_decoder.c index 8d5e8f63a5..244331d764 100644 --- a/lib/rbcodec/codecs/libopus/celt/celt_decoder.c +++ b/lib/rbcodec/codecs/libopus/celt/celt_decoder.c @@ -372,7 +372,7 @@ static OPUS_INLINE void celt_sat(celt_sig *x, int n) #ifndef RESYNTH static #endif -void celt_synthesis(const CELTMode *mode, celt_norm *X, celt_sig * out_syn[], +ICODE_ATTR_OPUS void celt_synthesis(const CELTMode *mode, celt_norm *X, celt_sig * out_syn[], opus_val16 *oldBandE, int start, int effEnd, int C, int CC, int isTransient, int LM, int downsample, int silence, int arch) @@ -822,7 +822,7 @@ static void celt_decode_lost(CELTDecoder * OPUS_RESTRICT st, int N, int LM) RESTORE_STACK; } -int celt_decode_with_ec(CELTDecoder * OPUS_RESTRICT st, const unsigned char *data, +ICODE_ATTR_OPUS int celt_decode_with_ec(CELTDecoder * OPUS_RESTRICT st, const unsigned char *data, int len, opus_val16 * OPUS_RESTRICT pcm, int frame_size, ec_dec *dec, int accum) { int c, i, N; diff --git a/lib/rbcodec/codecs/libopus/celt/cwrs.c b/lib/rbcodec/codecs/libopus/celt/cwrs.c index 12ff526ad7..96b59184c7 100644 --- a/lib/rbcodec/codecs/libopus/celt/cwrs.c +++ b/lib/rbcodec/codecs/libopus/celt/cwrs.c @@ -536,7 +536,7 @@ static opus_val32 cwrsi(int _n,int _k,opus_uint32 _i,int *_y){ return yy; } -opus_val32 decode_pulses(int *_y,int _n,int _k,ec_dec *_dec){ +ICODE_ATTR_OPUS opus_val32 decode_pulses(int *_y,int _n,int _k,ec_dec *_dec){ return cwrsi(_n,_k,ec_dec_uint(_dec,CELT_PVQ_V(_n,_k)),_y); } diff --git a/lib/rbcodec/codecs/libopus/celt/entcode.c b/lib/rbcodec/codecs/libopus/celt/entcode.c index 461a36dd55..1e3d7914c5 100644 --- a/lib/rbcodec/codecs/libopus/celt/entcode.c +++ b/lib/rbcodec/codecs/libopus/celt/entcode.c @@ -66,7 +66,7 @@ int ec_ilog(opus_uint32 _v){ /* This is a faster version of ec_tell_frac() that takes advantage of the low (1/8 bit) resolution to use just a linear function followed by a lookup to determine the exact transition thresholds. */ -opus_uint32 ec_tell_frac(ec_ctx *_this){ +ICODE_ATTR_OPUS opus_uint32 ec_tell_frac(ec_ctx *_this){ static const unsigned correction[8] = {35733, 38967, 42495, 46340, 50535, 55109, 60097, 65535}; diff --git a/lib/rbcodec/codecs/libopus/celt/entdec.c b/lib/rbcodec/codecs/libopus/celt/entdec.c index f706fa80ff..3607f4b0f8 100644 --- a/lib/rbcodec/codecs/libopus/celt/entdec.c +++ b/lib/rbcodec/codecs/libopus/celt/entdec.c @@ -99,7 +99,7 @@ static int ec_read_byte_from_end(ec_dec *_this){ /*Normalizes the contents of val and rng so that rng lies entirely in the high-order symbol.*/ -static void ec_dec_normalize(ec_dec *_this){ +ICODE_ATTR_OPUS static void ec_dec_normalize(ec_dec *_this){ /*If the range is too small, rescale it and input some bits.*/ while(_this->rng<=EC_CODE_BOT){ int sym; @@ -170,7 +170,7 @@ from8: # define EC_DEC_DIV(_n,_d,_ft) ((unsigned)((_n)/(_d))) #endif -unsigned ec_decode(ec_dec *_this,unsigned _ft){ +ICODE_ATTR_OPUS unsigned ec_decode(ec_dec *_this,unsigned _ft){ unsigned s; celt_sig_assert(_ft<=32768); _this->ext=celt_udiv(_this->rng,_ft); @@ -178,7 +178,7 @@ unsigned ec_decode(ec_dec *_this,unsigned _ft){ return _ft-EC_MINI(s+1,_ft); } -unsigned ec_decode_bin(ec_dec *_this,unsigned _bits){ +ICODE_ATTR_OPUS unsigned ec_decode_bin(ec_dec *_this,unsigned _bits){ unsigned s; celt_sig_assert(_bits<=15); _this->ext=_this->rng>>_bits; @@ -186,7 +186,7 @@ unsigned ec_decode_bin(ec_dec *_this,unsigned _bits){ return (1U<<_bits)-EC_MINI(s+1U,1U<<_bits); } -void ec_dec_update(ec_dec *_this,unsigned _fl,unsigned _fh,unsigned _ft){ +ICODE_ATTR_OPUS void ec_dec_update(ec_dec *_this,unsigned _fl,unsigned _fh,unsigned _ft){ opus_uint32 s; s=IMUL32(_this->ext,_ft-_fh); _this->val-=s; @@ -195,7 +195,7 @@ void ec_dec_update(ec_dec *_this,unsigned _fl,unsigned _fh,unsigned _ft){ } /*The probability of having a "one" is 1/(1<<_logp).*/ -int ec_dec_bit_logp(ec_dec *_this,unsigned _logp){ +ICODE_ATTR_OPUS int ec_dec_bit_logp(ec_dec *_this,unsigned _logp){ opus_uint32 r; opus_uint32 d; opus_uint32 s; @@ -231,7 +231,7 @@ int ec_dec_icdf(ec_dec *_this,const unsigned char *_icdf,unsigned _ftb){ return ret; } -opus_uint32 ec_dec_uint(ec_dec *_this,opus_uint32 _ft){ +ICODE_ATTR_OPUS opus_uint32 ec_dec_uint(ec_dec *_this,opus_uint32 _ft){ unsigned ft; unsigned s; int ftb; @@ -258,7 +258,7 @@ opus_uint32 ec_dec_uint(ec_dec *_this,opus_uint32 _ft){ } } -opus_uint32 ec_dec_bits(ec_dec *_this,unsigned _bits){ +ICODE_ATTR_OPUS opus_uint32 ec_dec_bits(ec_dec *_this,unsigned _bits){ ec_window window; int available; opus_uint32 ret; diff --git a/lib/rbcodec/codecs/libopus/celt/kiss_fft.c b/lib/rbcodec/codecs/libopus/celt/kiss_fft.c index 664f14e4cf..fb6b81c1db 100644 --- a/lib/rbcodec/codecs/libopus/celt/kiss_fft.c +++ b/lib/rbcodec/codecs/libopus/celt/kiss_fft.c @@ -575,7 +575,7 @@ void opus_fft_free(const kiss_fft_state *cfg, int arch) #endif /* CUSTOM_MODES */ -void opus_fft_impl(const kiss_fft_state *st,kiss_fft_cpx *fout) +ICODE_ATTR_OPUS_MR void opus_fft_impl(const kiss_fft_state *st,kiss_fft_cpx *fout) { int m2, m; int p; @@ -630,7 +630,7 @@ void opus_fft_impl(const kiss_fft_state *st,kiss_fft_cpx *fout) between the four transform lengths. */ static const kiss_fft_state pfa_st32 = { 0, 0, 0, 0, {0}, 0, pfa_tw32, 0 }; -void opus_pfa_impl(const kiss_fft_cpx *fin, +ICODE_ATTR_OPUS void opus_pfa_impl(const kiss_fft_cpx *fin, kiss_fft_cpx *fout, int nfft) { const opus_int16 *brev; diff --git a/lib/rbcodec/codecs/libopus/celt/laplace.c b/lib/rbcodec/codecs/libopus/celt/laplace.c index a7bca874b6..114186d4e6 100644 --- a/lib/rbcodec/codecs/libopus/celt/laplace.c +++ b/lib/rbcodec/codecs/libopus/celt/laplace.c @@ -91,7 +91,7 @@ void ec_laplace_encode(ec_enc *enc, int *value, unsigned fs, int decay) ec_encode_bin(enc, fl, fl+fs, 15); } -int ec_laplace_decode(ec_dec *dec, unsigned fs, int decay) +ICODE_ATTR_OPUS int ec_laplace_decode(ec_dec *dec, unsigned fs, int decay) { int val=0; unsigned fl; diff --git a/lib/rbcodec/codecs/libopus/celt/mathops.c b/lib/rbcodec/codecs/libopus/celt/mathops.c index 6ee9b9e101..65bc434e32 100644 --- a/lib/rbcodec/codecs/libopus/celt/mathops.c +++ b/lib/rbcodec/codecs/libopus/celt/mathops.c @@ -40,7 +40,7 @@ /*Compute floor(sqrt(_val)) with exact arithmetic. _val must be greater than 0. This has been tested on all possible 32-bit inputs greater than 0.*/ -unsigned isqrt32(opus_uint32 _val){ +ICODE_ATTR_OPUS unsigned isqrt32(opus_uint32 _val){ unsigned b; unsigned g; int bshift; @@ -88,7 +88,7 @@ opus_val32 frac_div32(opus_val32 a, opus_val32 b) } /** Reciprocal sqrt approximation in the range [0.25,1) (Q16 in, Q14 out) */ -opus_val16 celt_rsqrt_norm(opus_val32 x) +ICODE_ATTR_OPUS opus_val16 celt_rsqrt_norm(opus_val32 x) { opus_val16 n; opus_val16 r; diff --git a/lib/rbcodec/codecs/libopus/celt/quant_bands.c b/lib/rbcodec/codecs/libopus/celt/quant_bands.c index 39a221eda5..6b1598107a 100644 --- a/lib/rbcodec/codecs/libopus/celt/quant_bands.c +++ b/lib/rbcodec/codecs/libopus/celt/quant_bands.c @@ -425,7 +425,7 @@ void quant_energy_finalise(const CELTMode *m, int start, int end, opus_val16 *ol } } -void unquant_coarse_energy(const CELTMode *m, int start, int end, opus_val16 *oldEBands, int intra, ec_dec *dec, int C, int LM) +ICODE_ATTR_OPUS void unquant_coarse_energy(const CELTMode *m, int start, int end, opus_val16 *oldEBands, int intra, ec_dec *dec, int C, int LM) { const unsigned char *prob_model = e_prob_model[LM][intra]; int i, c; @@ -490,7 +490,7 @@ void unquant_coarse_energy(const CELTMode *m, int start, int end, opus_val16 *ol } } -void unquant_fine_energy(const CELTMode *m, int start, int end, opus_val16 *oldEBands, int *fine_quant, ec_dec *dec, int C) +ICODE_ATTR_OPUS void unquant_fine_energy(const CELTMode *m, int start, int end, opus_val16 *oldEBands, int *fine_quant, ec_dec *dec, int C) { int i, c; /* Decode finer resolution */ diff --git a/lib/rbcodec/codecs/libopus/celt/rate.c b/lib/rbcodec/codecs/libopus/celt/rate.c index 465e1ba26c..f06f477d12 100644 --- a/lib/rbcodec/codecs/libopus/celt/rate.c +++ b/lib/rbcodec/codecs/libopus/celt/rate.c @@ -529,7 +529,7 @@ static OPUS_INLINE int interp_bits2pulses(const CELTMode *m, int start, int end, return codedBands; } -int clt_compute_allocation(const CELTMode *m, int start, int end, const int *offsets, const int *cap, int alloc_trim, int *intensity, int *dual_stereo, +ICODE_ATTR_OPUS int clt_compute_allocation(const CELTMode *m, int start, int end, const int *offsets, const int *cap, int alloc_trim, int *intensity, int *dual_stereo, opus_int32 total, opus_int32 *balance, int *pulses, int *ebits, int *fine_priority, int C, int LM, ec_ctx *ec, int encode, int prev, int signalBandwidth) { int lo, hi, len, j; diff --git a/lib/rbcodec/codecs/libopus/celt/static_modes_fixed.h b/lib/rbcodec/codecs/libopus/celt/static_modes_fixed.h index d43ca72a7f..0b948fad4c 100644 --- a/lib/rbcodec/codecs/libopus/celt/static_modes_fixed.h +++ b/lib/rbcodec/codecs/libopus/celt/static_modes_fixed.h @@ -101,7 +101,9 @@ static const unsigned char cache_caps50[168] ICONST_ATTR = { #ifndef FFT_TWIDDLES48000_960 #define FFT_TWIDDLES48000_960 -static const kiss_twiddle_cpx fft_twiddles48000_960[480] ICONST_ATTR = { +/* Read only by the mixed-radix butterflies, which the prime factor + transform replaces; ICONST_ATTR_OPUS_MR drops it out of IRAM there. */ +static const kiss_twiddle_cpx fft_twiddles48000_960[480] ICONST_ATTR_OPUS_MR = { {32767, 0}, {32766, -429}, {32757, -858}, {32743, -1287}, {32724, -1715}, {32698, -2143}, diff --git a/lib/rbcodec/codecs/libopus/celt/vq.c b/lib/rbcodec/codecs/libopus/celt/vq.c index 7ba94fcbe5..1c2aa776bb 100644 --- a/lib/rbcodec/codecs/libopus/celt/vq.c +++ b/lib/rbcodec/codecs/libopus/celt/vq.c @@ -43,7 +43,7 @@ #endif #ifndef OVERRIDE_vq_exp_rotation1 -static void exp_rotation1(celt_norm *X, int len, int stride, opus_val16 c, opus_val16 s) +ICODE_ATTR_OPUS static void exp_rotation1(celt_norm *X, int len, int stride, opus_val16 c, opus_val16 s) { int i; opus_val16 ms; @@ -70,7 +70,7 @@ static void exp_rotation1(celt_norm *X, int len, int stride, opus_val16 c, opus_ } #endif /* OVERRIDE_vq_exp_rotation1 */ -void exp_rotation(celt_norm *X, int len, int dir, int stride, int K, int spread) +ICODE_ATTR_OPUS void exp_rotation(celt_norm *X, int len, int dir, int stride, int K, int spread) { static const int SPREAD_FACTOR[3]={15,10,5}; int i; @@ -117,7 +117,7 @@ void exp_rotation(celt_norm *X, int len, int dir, int stride, int K, int spread) /** Takes the pitch vector and the decoded residual vector, computes the gain that will give ||p+g*y||=1 and mixes the residual with the pitch. */ -static void normalise_residual(int * OPUS_RESTRICT iy, celt_norm * OPUS_RESTRICT X, +ICODE_ATTR_OPUS static void normalise_residual(int * OPUS_RESTRICT iy, celt_norm * OPUS_RESTRICT X, int N, opus_val32 Ryy, opus_val16 gain) { int i; @@ -364,7 +364,7 @@ unsigned alg_quant(celt_norm *X, int N, int K, int spread, int B, ec_enc *enc, /** Decode pulse vector and combine the result with the pitch vector to produce the final normalised signal in the current band. */ -unsigned alg_unquant(celt_norm *X, int N, int K, int spread, int B, +ICODE_ATTR_OPUS unsigned alg_unquant(celt_norm *X, int N, int K, int spread, int B, ec_dec *dec, opus_val16 gain) { opus_val32 Ryy; @@ -384,7 +384,7 @@ unsigned alg_unquant(celt_norm *X, int N, int K, int spread, int B, } #ifndef OVERRIDE_renormalise_vector -void renormalise_vector(celt_norm *X, int N, opus_val16 gain, int arch) +ICODE_ATTR_OPUS void renormalise_vector(celt_norm *X, int N, opus_val16 gain, int arch) { int i; #ifdef FIXED_POINT diff --git a/lib/rbcodec/codecs/libopus/config.h b/lib/rbcodec/codecs/libopus/config.h index 72cfb45d0e..d1d37e042e 100644 --- a/lib/rbcodec/codecs/libopus/config.h +++ b/lib/rbcodec/codecs/libopus/config.h @@ -23,6 +23,27 @@ rather than miscompiling. */ #define CELT_DECODE_ONLY +/* Code in IRAM. PP5022 and PP5024 give a codec an 80 KB IRAM window and Opus + already spends 39 KB of it on tables and .ibss; the hot decode path is + about 34 KB of code, so it fits with room over. Every other PP has a 48 KB + window, which the tables would leave too little of, and the AS3525 codec + link defines no IRAM region at all because the whole codec already runs + from IRAM there. libcook splits the same way, for the same reason. + Measured on a Sansa e200v1: 48.96 MHz to 42.92, a 12.3% cut that the cycle + model cannot see at all, because it does not model an instruction cache. + Define OPUS_NO_ICODE to build without it and compare. */ +#if (CONFIG_CPU == PP5022 || CONFIG_CPU == PP5024) && !defined(OPUS_NO_ICODE) +#define OPUS_ARM_ICODE /* also read by the .S kernels */ +#endif + +#ifndef __ASSEMBLER__ +#ifdef OPUS_ARM_ICODE +#define ICODE_ATTR_OPUS ICODE_ATTR +#else +#define ICODE_ATTR_OPUS +#endif +#endif + /* alloc stuff */ #define VAR_ARRAYS #define NORM_ALIASING_HACK @@ -90,6 +111,16 @@ #define OPUS_PFA #endif +/* Mixed-radix leftovers: built, but not worth IRAM once the prime + factor transform makes them unreachable. */ +#ifdef OPUS_PFA +#define ICODE_ATTR_OPUS_MR +#define ICONST_ATTR_OPUS_MR +#else +#define ICODE_ATTR_OPUS_MR ICODE_ATTR_OPUS +#define ICONST_ATTR_OPUS_MR ICONST_ATTR +#endif + #if defined(CPU_COLDFIRE) #define OPUS_CF_INLINE_ASM #endif From 93d564426bcc1b27514665b72c38a019cf46ba71 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:29 -0400 Subject: [PATCH 238/408] opus: ARMv4 exp_rotation1 At stride 1 the rotation chain writes X[i+stride] and reads it straight back, so one load an iteration is redundant and one store is dead. The kernel carries that value, narrowed, since mul reads all 32 bits where smulbb does not. Bit-exact over 8.2M samples. Modelled: -0.87% ARMv4, the function -10.0%. Measured: e200v1 39.59 -> 39.19 MHz. Co-Authored-By: Claude Opus 5 Change-Id: Idbd1a088bc805fecfb9ee54fc372ad16a7d91606 --- lib/rbcodec/codecs/libopus/SOURCES | 1 + .../celt/arm/exp_rotation1_armv4_asm.S | 200 ++++++++++++++++++ lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h | 11 +- 3 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 lib/rbcodec/codecs/libopus/celt/arm/exp_rotation1_armv4_asm.S diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index 63c04f41bf..baf2751702 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -15,6 +15,7 @@ celt/arm/pfa_armv4_asm.S celt/arm/comb_filter_armv4_asm.S celt/arm/denorm_armv4_asm.S celt/arm/deemph_armv4_asm.S +celt/arm/exp_rotation1_armv4_asm.S #elif defined(CPU_ARM) && (ARM_ARCH >= 5) && (ARCH_PROFILE != ARM_PROFILE_MICRO) celt/arm/kiss_fft_armv5e_asm.S celt/arm/mdct_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/celt/arm/exp_rotation1_armv4_asm.S b/lib/rbcodec/codecs/libopus/celt/arm/exp_rotation1_armv4_asm.S new file mode 100644 index 0000000000..2666f7a2b2 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/celt/arm/exp_rotation1_armv4_asm.S @@ -0,0 +1,200 @@ +/* Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * celt/kiss_fft.c are met. + */ + +/* ARMv4 exp_rotation1, the largest single function in an ARM7TDMI decode. + * + * The chain of Givens rotations it applies writes X[i+stride] and then reads + * it back as the next iteration's x1, so at stride 1 -- 58% of the executed + * iterations -- one of the two loads is redundant and one of the two stores + * is dead: nothing reads that slot before the next iteration overwrites it, + * so its store waits until the chain ends. Both loops below carry that + * value in a register, forwards as the next x1 and backwards as the next x2. + * + * What ARMv4 has to do that ARMv5E does not is truncate it. The ARMv5E + * kernel carries the full 32-bit shifted value because every later use is + * the bottom halfword operand of smulbb, which reads exactly the bits that + * would have been stored. Here the use is mul, which reads all 32, so the + * carried value has to be the halfword EXTRACT16 would have written: + * + * mov rC, rY, lsl #1 @ bit 30 to bit 31 + * mov rC, rC, asr #16 @ bits 30..15, sign extended + * + * which is PSHR32(y,15) narrowed to 16 bits, in the two instructions that + * replace the one shift -- so the carry costs one cycle and saves four. + * + * The multiplies are the floor and are left alone. Every operand is a + * sign-extended halfword, so the multiplier finishes in two passes: the four + * of them are 14 cycles of the loop's 27, measured, and no rearrangement + * (including the three-multiply complex product) improves on that once the + * multiplier is credited with stopping early. + * + * A larger stride gets the plain loop. Its elements split into stride + * independent chains that could each be carried the same way, but the chains + * are about three steps long at the sizes CELT uses, and priming a carry and + * flushing its deferred store costs more than the three steps save. + * + * Bit-exact against the C, forwards and backwards, at every stride. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "exp_rotation1_armv4_asm.S must be assembled in ARM mode" +#endif + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#if defined(OPUS_ARM_ICODE) && !defined(OPUS_NO_ICODE) + .section .icode,"ax",%progbits +#else + .text +#endif + .align 2 + +/* One carried step of the forward chain: x1 is in r7 and stays there as the + next iteration's x1, and the store of X[i+1] waits until the chain ends + because the next iteration reads it out of the register. */ + .macro ER4_FWD + ldrsh r8, [r10, #2] @ x2 = X[i+1] + mul ip, r4, r7 @ s*x1, and the gap before x2 is used + mul r9, r5, r8 @ ms*x2 + mla ip, r3, r8, ip @ c*x2 + s*x1 + mla r9, r3, r7, r9 @ c*x1 - s*x2 + add ip, ip, #0x4000 + add r9, r9, #0x4000 + mov r7, ip, lsl #1 @ X[i+1], carried as the next x1, + mov r7, r7, asr #16 @ narrowed as EXTRACT16 would store it + mov r9, r9, asr #15 + strh r9, [r10], #2 @ X[i] + .endm + +/* The same backwards, where the carried value is X[i] and becomes the next + iteration's x2. */ + .macro ER4_BWD + ldrsh r7, [r10, #-2]! @ x1 = X[i], r10 = &X[i] + mul r9, r5, r8 @ ms*x2, and the gap before x1 is used + mul ip, r4, r7 @ s*x1 + mla r9, r3, r7, r9 @ c*x1 - s*x2 + mla ip, r3, r8, ip @ c*x2 + s*x1 + add r9, r9, #0x4000 + add ip, ip, #0x4000 + mov r8, r9, lsl #1 @ X[i], carried as the next x2 + mov r8, r8, asr #16 + mov ip, ip, asr #15 + strh ip, [r10, #2] @ X[i+1] + .endm + +/* ------------------------------------------------------------------------ + * void exp_rotation1_armv4(celt_norm *X, int len, int stride, + * opus_val16 c, opus_val16 s) + * + * Forward over i = 0 .. len-stride-1, then backward over + * i = len-2*stride-1 .. 0, each step + * x1 = X[i], x2 = X[i+stride] + * X[i+stride] = (c*x2 + s*x1 + 16384) >> 15 + * X[i] = (c*x1 - s*x2 + 16384) >> 15 + * + * r0 X, r1 len, r2 stride, r3 c, r4 s, r5 ms, r6 count, r7 x1, r8 x2, + * r9 and ip the two accumulators, r10 the walking pointer. + * ------------------------------------------------------------------------ */ + .global exp_rotation1_armv4 + .type exp_rotation1_armv4, %function +exp_rotation1_armv4: + push {r4-r10, lr} + ldrsh r4, [sp, #32] @ s + rsb r5, r4, #0 @ ms = NEG16(s), and narrowed to sixteen + mov r5, r5, lsl #16 @ bits so that it wraps where NEG16 does. + mov r5, r5, asr #16 @ mul reads all 32 bits, unlike smulbb, + @ so the ARMv5E kernel can skip this and + @ this one cannot. + cmp r2, #1 + bne .Ler4_general + + /* ---- stride 1, forward: carry X[i+1] into the next x1 ---- */ + subs r6, r1, #1 @ len-1 iterations + ble .Ler4_back1 + mov r10, r0 + ldrsh r7, [r10] @ x1 = X[0] + tst r6, #1 + beq .Ler4_f1 @ even already + ER4_FWD @ peel one, so the pairs loop divides + subs r6, r6, #1 + beq .Ler4_f1_end +.Ler4_f1: + ER4_FWD + ER4_FWD + subs r6, r6, #2 + bne .Ler4_f1 +.Ler4_f1_end: + strh r7, [r10] @ the last X[i+1] + + /* ---- stride 1, backward: carry X[i] into the next x2 ---- */ +.Ler4_back1: + subs r6, r1, #2 @ len-2 iterations + ble .Ler4_done + add r10, r0, r6, lsl #1 @ &X[len-2] + ldrsh r8, [r10] @ x2 = X[len-2] + tst r6, #1 + beq .Ler4_b1 + ER4_BWD + subs r6, r6, #1 + beq .Ler4_b1_end +.Ler4_b1: + ER4_BWD + ER4_BWD + subs r6, r6, #2 + bne .Ler4_b1 +.Ler4_b1_end: + strh r8, [r10] @ the last X[i], which is X[0] + b .Ler4_done + + /* ---- any other stride: both loads, both stores ---- */ +.Ler4_general: + mov r2, r2, lsl #1 @ stride in bytes, numerically 2*stride + subs r6, r1, r2, lsr #1 @ len-stride iterations + ble .Ler4_backg + mov r10, r0 +.Ler4_fg: + ldrsh r7, [r10] @ x1 + ldrsh r8, [r10, r2] @ x2 + mul ip, r4, r7 @ s*x1 + mul r9, r5, r8 @ ms*x2 + mla ip, r3, r8, ip @ c*x2 + s*x1 + mla r9, r3, r7, r9 @ c*x1 - s*x2 + add ip, ip, #0x4000 + add r9, r9, #0x4000 + mov ip, ip, asr #15 + mov r9, r9, asr #15 + strh ip, [r10, r2] @ X[i+stride] + subs r6, r6, #1 + strh r9, [r10], #2 @ X[i] + bne .Ler4_fg +.Ler4_backg: + subs r6, r1, r2 @ len-2*stride iterations + ble .Ler4_done + sub r10, r6, #1 + add r10, r0, r10, lsl #1 @ &X[len-2*stride-1] +.Ler4_bg: + ldrsh r7, [r10] + ldrsh r8, [r10, r2] + mul ip, r4, r7 + mul r9, r5, r8 + mla ip, r3, r8, ip + mla r9, r3, r7, r9 + add ip, ip, #0x4000 + add r9, r9, #0x4000 + mov ip, ip, asr #15 + mov r9, r9, asr #15 + strh ip, [r10, r2] + subs r6, r6, #1 + strh r9, [r10], #-2 + bne .Ler4_bg +.Ler4_done: + pop {r4-r10, pc} + .size exp_rotation1_armv4, .-exp_rotation1_armv4 + + .section .note.GNU-stack,"",%progbits diff --git a/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h index d45454fc61..a68b9946ec 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h @@ -31,7 +31,7 @@ chain writes X[i+stride] and reads it straight back as the next x1, so at stride 1 one of the two loads is redundant and one of the two stores is dead, and both kernels carry that value in a register instead. Bit-exact; - see celt/arm/exp_rotation1_armv5e_asm.S. + see celt/arm/exp_rotation1_armv5e_asm.S and _armv4_asm.S. Shares OPUS_ARM_NO_BANDS_ASM with arm/bands_arm.h as the escape hatch. */ @@ -51,6 +51,15 @@ void normres_scale_armv5e(celt_norm *X, const int *iy, int N, int g, int shift); #define NORMRES_SCALE normres_scale_armv5e +#elif defined(FIXED_POINT) && !defined(OPUS_ARM_NO_BANDS_ASM) \ + && defined(OPUS_ARM_INLINE_ASM) && (ARM_ARCH == 4) + +#define OVERRIDE_vq_exp_rotation1 +void exp_rotation1_armv4(celt_norm *X, int len, int stride, opus_val16 c, + opus_val16 s); +#define exp_rotation1(X, len, stride, c, s) \ + exp_rotation1_armv4((X), (len), (stride), (c), (s)) + #endif #endif /* VQ_ARM_H */ From 5a6935047e6c43ebb4dfb6bdf805474fa044c82a Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 18 Sep 2026 11:59:30 -0400 Subject: [PATCH 239/408] opus: hold cwrsi's row pair, and test it with one comparison 53% of the dimensions a decode walks hold no pulses, and that arm leaves _k alone, so the two CELT_PVQ_U_ROW pointers stay valid. U is non-decreasing in _k, so p <= _i < q is the single unsigned test (_i-p) < (q-p). Bit-exact over 8.2M samples. Modelled: -0.40% ARMv4, -0.78% ARMv5E; the function -4.8% and -7.1%. Measured: e200v1 39.19 -> 38.96 MHz, Clip+ 28.13 -> 28.08 MHz. Co-Authored-By: Claude Opus 5 Change-Id: Id50be222e101aeabd9a1f4cf5380d3244da610ff --- lib/rbcodec/codecs/libopus/celt/cwrs.c | 40 ++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/lib/rbcodec/codecs/libopus/celt/cwrs.c b/lib/rbcodec/codecs/libopus/celt/cwrs.c index 96b59184c7..6bb8b9be3b 100644 --- a/lib/rbcodec/codecs/libopus/celt/cwrs.c +++ b/lib/rbcodec/codecs/libopus/celt/cwrs.c @@ -461,13 +461,21 @@ void encode_pulses(const int *_y,int _n,int _k,ec_enc *_enc){ } static opus_val32 cwrsi(int _n,int _k,opus_uint32 _i,int *_y){ + const opus_uint32 *rk; + const opus_uint32 *rk1; opus_uint32 p; + opus_uint32 d; int s; int k0; + int kc; opus_int16 val; opus_val32 yy=0; celt_assert(_k>0); celt_assert(_n>1); + /*_k never goes negative, so this cannot match before the pair has been + loaded once.*/ + kc=-1; + rk=rk1=NULL; while(_n>2){ opus_uint32 q; /*Lots of pulses case:*/ @@ -495,12 +503,32 @@ static opus_val32 cwrsi(int _n,int _k,opus_uint32 _i,int *_y){ } /*Lots of dimensions case:*/ else{ - /*Are there any pulses in this dimension at all?*/ - p=CELT_PVQ_U_ROW[_k][_n]; - q=CELT_PVQ_U_ROW[_k+1][_n]; - if(p<=_i&&_i=_n)break; + p=rk[_n]; + q=rk1[_n]; + d=_i-p; + if(d>=q-p)break; + } + /*_n is already decremented for every dimension handled above.*/ + continue; } else{ /*Are the pulses in this dimension negative?*/ From b12ef5e6f8fa0c74f7ea57ff656273b882d2e6cb Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Wed, 23 Sep 2026 19:58:16 -0400 Subject: [PATCH 240/408] opus: gate the ARMv5E kernels for ARMv6 too Four `ARM_ARCH == 5` checks -- in SOURCES and celt/arm/{bands_arm, comb_filter_arm,vq_arm}.h -- excluded ARMv6 from every ARMv5E kernel: denorm_band, haar1, comb_filter_const, celt_sat, deemphasis_stereo_simple, exp_rotation1 and normres_scale all silently fell back to plain C on ARM1136/ARM1176, since ARMv6 is a strict superset of the EDSP instructions those kernels use. Widened to ARM_ARCH >= 5, matching config.h's own OPUS_ARM_INLINE_EDSP gate, which was already ARM_ARCH > 4. These four can't be fixed at the commits that introduced them: those commits are already merged into master under different SHAs. A fifth instance of the same bug, in celt/arm/mdct_armv5e.h, was fixed at its origin commit instead, since that one is still open for review. Verified on both native ARMv6 targets (iPod Nano 4G, ARM1176JZ-S; Gigabeat S, ARM1136JF-S) and the hosted Samsung YP-R0 (ARM1176JZ-S, cross toolchain built for the occasion): all three now link and call all 16 ARMv5E kernels, where they linked and called zero before this fix. Decoded PCM bit-identical to the ARMv5E build under qemu. Co-Authored-By: Claude Sonnet 5 Change-Id: I7e6f6b421b9c0203812fb45d2258c7eb81738d98 --- lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h | 2 +- lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h | 2 +- lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h | 2 +- lib/rbcodec/codecs/libopus/config.h | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h index 4ff0952aa8..31dd2233af 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h @@ -42,7 +42,7 @@ void denorm_band_armv4(celt_sig *f, const celt_norm *x, int n, int g, int shift); # define DENORM_BAND denorm_band_armv4 -# elif defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH == 5) +# elif defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH >= 5) # define OVERRIDE_DENORM_BAND void denorm_band_armv5e(celt_sig *f, const celt_norm *x, int n, int g, diff --git a/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h index a1f1baaecf..67a0559047 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h @@ -61,7 +61,7 @@ void deemph_stereo_armv4(celt_sig *x0, celt_sig *x1, opus_val16 *pcm, int N, # define deemphasis_stereo_simple(in, pcm, N, coef0, mem) \ deemph_stereo_armv4((in)[0], (in)[1], (pcm), (N), (coef0), (mem)) -# elif defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH == 5) +# elif defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH >= 5) # define OVERRIDE_COMB_FILTER_CONST void comb_filter_const_armv5e(opus_val32 *y, opus_val32 *x, int T, int N, diff --git a/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h index a68b9946ec..6b725a3a47 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h @@ -36,7 +36,7 @@ Shares OPUS_ARM_NO_BANDS_ASM with arm/bands_arm.h as the escape hatch. */ #if defined(FIXED_POINT) && !defined(OPUS_ARM_NO_BANDS_ASM) \ - && defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH == 5) + && defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH >= 5) #define OVERRIDE_vq_exp_rotation1 void exp_rotation1_armv5e(celt_norm *X, int len, int stride, opus_val16 c, diff --git a/lib/rbcodec/codecs/libopus/config.h b/lib/rbcodec/codecs/libopus/config.h index d1d37e042e..3b57bc4221 100644 --- a/lib/rbcodec/codecs/libopus/config.h +++ b/lib/rbcodec/codecs/libopus/config.h @@ -87,6 +87,8 @@ #define OPUS_ARM_NO_MDCT_ASM #define OPUS_ARM_NO_PITCH_ASM #define OPUS_NO_PFA +#define OPUS_ARM_NO_COMB_ASM +#define OPUS_ARM_NO_BANDS_ASM #endif #endif /* ARM_ARCH */ From 9aa2d7fe9450bee3b0d72a0f62c9f5a2367a723c Mon Sep 17 00:00:00 2001 From: Paul Sauro Date: Sun, 20 Sep 2026 17:21:42 +0200 Subject: [PATCH 241/408] usb: accept replacement SETUP after an abandoned control transfer A host can replace an unfinished control transfer with a new SETUP. ARC and DesignWare flush EP0 without reporting completion callbacks, leaving the core waiting for a data/status packet that will never arrive. Explicitly notify the core after both directions and stale completion bits have been cancelled. Return an abandoned data/status stage to READY. If a queued or running handler still owns the request and data buffer, keep that ownership and suppress its response; dispatch only the latest replacement when it returns. A driver-owned SET_ADDRESS can also cancel a deferred request without passing its SETUP to the core. Do not overwrite an arbitrary busy state in usb_core_setup_received(). ARC cancellation is a prerequisite in the preceding patch; DesignWare cancels both EP0 directions before notifying and rearming reception. Change-Id: Ia8e0a68fe47ccab952168da24a2f76e311ca2b15 --- firmware/drivers/usb-designware.c | 7 +++++++ firmware/export/usb_core.h | 3 +++ firmware/target/arm/usb-drv-arc.c | 2 ++ firmware/usbstack/usb_core.c | 33 +++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/firmware/drivers/usb-designware.c b/firmware/drivers/usb-designware.c index 8440eafb1e..c4aa7262b5 100644 --- a/firmware/drivers/usb-designware.c +++ b/firmware/drivers/usb-designware.c @@ -901,7 +901,11 @@ static void usb_dw_handle_setup_received(void) #endif struct usb_ctrlrequest req; memcpy(&req, ep0_buffer, sizeof(struct usb_ctrlrequest)); + /* Abort both old data/status directions before delivering the new SETUP. + * Flush does not report completions, so notify the core explicitly once. */ usb_dw_flush_endpoint(0, USB_DW_EPDIR_IN); + usb_dw_flush_endpoint(0, USB_DW_EPDIR_OUT); + usb_core_control_cancelled(); usb_dw_ep0_recv(); if(!handle_set_address(&req)) @@ -1004,6 +1008,9 @@ static void usb_dw_oepint(int ep) if (epints & STUP) { usb_dw_handle_setup_received(); + /* SETUP cancelled the old transfer, including any XFRC captured + * in this interrupt snapshot. Do not complete the new request. */ + return; } if (epints & XFRC) diff --git a/firmware/export/usb_core.h b/firmware/export/usb_core.h index d59c980861..60a7c80f3a 100644 --- a/firmware/export/usb_core.h +++ b/firmware/export/usb_core.h @@ -55,6 +55,9 @@ struct usb_class_driver; void usb_core_init(void); void usb_core_exit(void); +/* IRQ context, after both EP0 directions and their stale completions have + * been cancelled. Does not release a request buffer still owned by a handler. */ +void usb_core_control_cancelled(void); void usb_core_setup_received(struct usb_ctrlrequest* req); void usb_core_control_response(enum usb_control_response response, const void* data, size_t size); void usb_core_transfer_complete(int endpoint,int dir,int status,int length); diff --git a/firmware/target/arm/usb-drv-arc.c b/firmware/target/arm/usb-drv-arc.c index 506b656400..166b6eccc1 100644 --- a/firmware/target/arm/usb-drv-arc.c +++ b/firmware/target/arm/usb-drv-arc.c @@ -1102,6 +1102,8 @@ static void setup_received(void) } } + usb_core_control_cancelled(); + struct usb_ctrlrequest *req = (struct usb_ctrlrequest*)tmp; /* A new setup packet supersedes any unfinished SET_ADDRESS request. */ diff --git a/firmware/usbstack/usb_core.c b/firmware/usbstack/usb_core.c index efe123cc41..acccee21d2 100644 --- a/firmware/usbstack/usb_core.c +++ b/firmware/usbstack/usb_core.c @@ -225,6 +225,7 @@ enum { EP0_EXPECT_RX_STATUS_COMP, /* sending status */ }; static volatile int ep0_state; +static bool control_cancelled; #define TRACE_EP0_STATE 0 #if TRACE_EP0_STATE == 1 @@ -1193,6 +1194,7 @@ static void signal_xfer_complete(int ep, struct usb_ctrlrequest* req, int status } static void process_setup_request(struct usb_ctrlrequest* req) { + control_cancelled = false; set_ep0_state(req->bRequestType & USB_DIR_IN ? EP0_HANDLING_TX_CONTROL : EP0_HANDLING_RX_CONTROL); if(ep0_state == EP0_HANDLING_TX_CONTROL || req->wLength == 0) { @@ -1204,6 +1206,7 @@ static void process_setup_request(struct usb_ctrlrequest* req) { if(req->wLength > sizeof(usb_control_data)) { logf("usb_core: control write too large %u > %u", req->wLength, sizeof(usb_control_data)); usb_drv_stall(EP_CONTROL, true, false); + set_ep0_state(EP0_READY); return; } set_ep0_state(EP0_EXPECT_RX_DATA_COMP); @@ -1320,17 +1323,32 @@ void usb_core_handle_notify(long id, intptr_t data) } } +void usb_core_control_cancelled(void) +{ + /* A newer SETUP supersedes even a request deferred behind a handler. */ + have_pending_request = false; + /* A queued/running handler still owns handling_request and the shared + * buffer. Suppress its response; a later SETUP may replace it on return. */ + if(ep0_state == EP0_HANDLING_TX_CONTROL || ep0_state == EP0_HANDLING_RX_CONTROL) + control_cancelled = true; + else + set_ep0_state(EP0_READY); +} + void usb_core_setup_received(struct usb_ctrlrequest* req) { if(bus_reset_pending) { logf("usb_core: bus resetting tick=%lu", current_tick); return; } + /* Drivers cancel abandoned EP0 transfers before delivering replacement + * SETUP. A handler may still own the request buffer; defer until it returns. */ if(ep0_state != EP0_READY) { logf("usb_core: control pending tick=%lu", current_tick); pending_request = *req; have_pending_request = true; return; } + have_pending_request = false; handling_request = *req; process_setup_request(&handling_request); } @@ -1338,13 +1356,27 @@ void usb_core_setup_received(struct usb_ctrlrequest* req) { void usb_core_control_response(enum usb_control_response response, const void* data, size_t size) { logf("usb_core: response ack=%d size=%u ep0_state=%d tick=%lu", response, size, ep0_state, current_tick); + int oldlevel = disable_irq_save(); + if(bus_reset_pending) { + restore_irq(oldlevel); + return; + } if((ep0_state == EP0_HANDLING_TX_CONTROL || ep0_state == EP0_HANDLING_RX_CONTROL) && check_for_new_setup()) { + restore_irq(oldlevel); + return; + } + + if(control_cancelled) { + control_cancelled = false; + set_ep0_state(EP0_READY); + restore_irq(oldlevel); return; } if(response == USB_CONTROL_STALL) { set_ep0_state(EP0_READY); usb_drv_stall(EP_CONTROL, true, true); + restore_irq(oldlevel); return; } @@ -1381,6 +1413,7 @@ void usb_core_control_response(enum usb_control_response response, const void* d panicf("usb_core: invalid control response ep_state=%d", ep0_state); break; } + restore_irq(oldlevel); } void usb_core_notify_set_address(uint8_t addr) From 636ae404f78e48fe3a9bfb835da341772bf71593 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 27 Sep 2026 10:40:58 -0400 Subject: [PATCH 242/408] misc: forums.rockbox.org -> www.rockbox.org/forums Change-Id: Ieddb1d534aa15e3e931eb66e9380276602cfd0c5 --- apps/plugins/lua/include_lua/rbsettings.lua | 2 +- apps/plugins/pixel-painter.lua | 2 +- manual/appendix/appendix.tex | 2 +- manual/getting_started/installation.tex | 2 +- manual/getting_started/main.tex | 2 +- utils/rockbox_api/index.tpl | 38 ++++++++++----------- utils/rockbox_api/section.tpl | 38 ++++++++++----------- 7 files changed, 43 insertions(+), 43 deletions(-) diff --git a/apps/plugins/lua/include_lua/rbsettings.lua b/apps/plugins/lua/include_lua/rbsettings.lua index 7f9d00982f..2aaf06500f 100644 --- a/apps/plugins/lua/include_lua/rbsettings.lua +++ b/apps/plugins/lua/include_lua/rbsettings.lua @@ -18,7 +18,7 @@ * KIND, either express or implied. * ****************************************************************************/ -see https://forums.rockbox.org/index.php/topic,55820.0.html for an example +see https://www.rockbox.org/forums/index.php/topic,55820.0.html for an example see rocklib.c for definitive list of supported settings rb.global_settings() rb.global_status() diff --git a/apps/plugins/pixel-painter.lua b/apps/plugins/pixel-painter.lua index f237cc1c25..f3e22a78d9 100644 --- a/apps/plugins/pixel-painter.lua +++ b/apps/plugins/pixel-painter.lua @@ -90,7 +90,7 @@ function calculate_par(board) return moves end --Manual garbage collection is needed so it doesn't eat into the - --audio buffer, see http://forums.rockbox.org/index.php/topic,27120.msg177434.html + --audio buffer, see https://www.rockbox.org/forums/index.php/topic,27120.msg177434.html collectgarbage("collect") moves = moves + 1 until false diff --git a/manual/appendix/appendix.tex b/manual/appendix/appendix.tex index 0305df4870..0af2ad49c6 100644 --- a/manual/appendix/appendix.tex +++ b/manual/appendix/appendix.tex @@ -38,7 +38,7 @@ files etc. (IMPORTANT!) \section{Feature ideas} To suggest an idea for a feature or to read those made by others, see -\url{https://forums.rockbox.org/index.php?board=49.0}. Please keep in +\url{https://www.rockbox.org/forums/index.php?board=49.0}. Please keep in mind that this forum is for the discussion of feature ideas -- they are not requests and there is no guarantee they will be acted upon. diff --git a/manual/getting_started/installation.tex b/manual/getting_started/installation.tex index 22ada06789..5ac714b051 100644 --- a/manual/getting_started/installation.tex +++ b/manual/getting_started/installation.tex @@ -570,7 +570,7 @@ that the download name includes "erosqnative"!}} the Rockbox source code repository and represents the current state of Rockbox development. This means that the build could contain bugs but most of the time is safe to use. For a manual install, you can download the current build from - \url{https://build.rockbox.org/}. + \url{https://www.rockbox.org/download/daily.shtml}. \item[Archived Build.] In addition to the release version and the current build, there is also an archive of daily builds available for download. These are diff --git a/manual/getting_started/main.tex b/manual/getting_started/main.tex index 825fa1ab49..29323de734 100644 --- a/manual/getting_started/main.tex +++ b/manual/getting_started/main.tex @@ -33,7 +33,7 @@ of call when looking for further help. If you cannot find the information you are searching for on the Rockbox website there are a number of support channels you should have a look at. -You can try the Rockbox forums located at \url{https://forums.rockbox.org/}. +You can try the Rockbox forums located at \url{https://www.rockbox.org/forums/}. The mailing lists are another option, and can be found at \url{https://www.rockbox.org/mail/}. From that page you can subscribe to the lists and browse the archives. To search the list archives simply use diff --git a/utils/rockbox_api/index.tpl b/utils/rockbox_api/index.tpl index ca8f602fd2..4d30f87fbb 100644 --- a/utils/rockbox_api/index.tpl +++ b/utils/rockbox_api/index.tpl @@ -1,9 +1,9 @@ - + - - + + Rockbox Plugin API - INDEX @@ -13,47 +13,47 @@
    -

    - Rockbox.org home +
    - releases
    - current build
    - extras + releases
    + current build
    + extras - FAQ
    - manual
    - wiki
    - docs index + FAQ
    + manual
    + wiki
    + docs index - mailing lists
    + mailing lists
    - IRC
    - forums + IRC
    + forums - bugs
    - patches
    + bugs
    + patches
    -
    +

    -
    +
    diff --git a/utils/rockbox_api/section.tpl b/utils/rockbox_api/section.tpl index 6fb019538b..c2bd7365b0 100644 --- a/utils/rockbox_api/section.tpl +++ b/utils/rockbox_api/section.tpl @@ -1,9 +1,9 @@ - + - - + + Rockbox Plugin API - %GROUP_NAME% @@ -14,46 +14,46 @@
    - releases
    - current build
    - extras + releases
    + current build
    + extras - FAQ
    - manual
    - wiki
    - docs index + FAQ
    + manual
    + wiki
    + docs index - mailing lists
    + mailing lists
    - IRC
    - forums + IRC
    + forums - bugs
    - patches
    + bugs
    + patches
    - +

    -
    +
    From e2efcb906abe2b2cffdd1008285fc36b696b583e Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 27 Sep 2026 11:06:40 -0400 Subject: [PATCH 243/408] manual: Correct download links for daily/dev builds. Change-Id: Iba3822386d2ed7c2f91d635f63014d63b652decc --- manual/getting_started/installation.tex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manual/getting_started/installation.tex b/manual/getting_started/installation.tex index 5ac714b051..8a78b84fbd 100644 --- a/manual/getting_started/installation.tex +++ b/manual/getting_started/installation.tex @@ -570,12 +570,12 @@ that the download name includes "erosqnative"!}} the Rockbox source code repository and represents the current state of Rockbox development. This means that the build could contain bugs but most of the time is safe to use. For a manual install, you can download the current build from - \url{https://www.rockbox.org/download/daily.shtml}. + \url{https://www.rockbox.org/download/devbuilds.html}. -\item[Archived Build.] In addition to the release version and the current build, +\item[Archived Daily Build.] In addition to the release version and the current build, there is also an archive of daily builds available for download. These are built once a day from the latest source code in the repository. For a manual install, - you can download archived builds from \url{https://www.rockbox.org/daily.shtml}. + you can download archived builds from \url{https://www.rockbox.org/download/daily.shtml}. \end{description} From f5076456fe949e149c0ed9e3c6a0ba042671947c Mon Sep 17 00:00:00 2001 From: neofright Date: Sun, 27 Sep 2026 22:03:30 +0100 Subject: [PATCH 244/408] Fix path bug in bootloader/hibyos_linux.c run_file() joins /mnt/sd_0 and the filename without a /, so /bin/sh cannot open any script and always returns 2. The path bug has been fixed and the buffer size adjusted and freed after execution. GPT 6 Sol did the work. I'm just the meat proxy. Change-Id: I644a773b2bd707e0ee2d8c70c91b7a6075cf2622 --- bootloader/hibyos_linux.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bootloader/hibyos_linux.c b/bootloader/hibyos_linux.c index 449ff6bd76..d05259536e 100644 --- a/bootloader/hibyos_linux.c +++ b/bootloader/hibyos_linux.c @@ -467,9 +467,12 @@ int choice_screen(const char *title, bool center, int nr_choices, const char *ch void run_file(const char *name) { - char *dirname = BASE_DIR; - char *buf = malloc(strlen(dirname) + strlen(name) + 1); - sprintf(buf, "%s%s", dirname, name); + const char *dirname = BASE_DIR; + size_t path_size = strlen(dirname) + strlen(name) + 2; + char *buf = malloc(path_size); + if (!buf) + return; + snprintf(buf, path_size, "%s/%s", dirname, name); lcd_clear_display(); lcd_set_foreground(LCD_RGBPACK(255, 201, 0)); @@ -484,6 +487,7 @@ void run_file(const char *name) } int status; waitpid(pid, &status, 0); + free(buf); if(WIFEXITED(status)) { lcd_set_foreground(LCD_RGBPACK(255, 201, 0)); From 583f27d5e5a349837eaa3d323a132b85b6fcd8cd Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 27 Sep 2026 19:35:47 -0400 Subject: [PATCH 245/408] FS#14002 - German Language Patch (Wilfried Winkler) Change-Id: I83d42092b192eb93df680bd98c724d81f65ddca1 --- apps/lang/deutsch.lang | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/lang/deutsch.lang b/apps/lang/deutsch.lang index 6f3db1d509..9e6cebca59 100644 --- a/apps/lang/deutsch.lang +++ b/apps/lang/deutsch.lang @@ -17126,10 +17126,10 @@ *: "Filter %d" - *: "Filter %d" + *: "~Filter %d" - *: "Filter" + *: "~Filter" From 2745e0b973bf4f2982ff3216be2e45d1e9bd4b4c Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 27 Sep 2026 19:01:35 -0400 Subject: [PATCH 246/408] FS#14011: Handle usb hosts whose first request is SET_ADDRESS (Anthony Fletcher) Thanks for the SET_ADDRESS rework. I think it leaves one case uncovered: a host whose first request is SET_ADDRESS ends up with a device whose interfaces are all numbered 0. Two things still depend on the core seeing a request before the address arrives: 1. usb_core assigns interfaces and endpoints only on the first control request it handles in DEFAULT. When SET_ADDRESS comes first, the driver completes it and usb_core_set_address() moves the state to ADDRESS. So allocate_interfaces_and_endpoints() never runs. 2. Under USB_DETECT_BY_REQUEST, usb.c enables the class drivers only on a USB_TRANSFER_COMPLETION event. The SET_ADDRESS status stage now completes inside the driver, so the drivers are still disabled when the address arrives. Change-Id: Ifacb8b07cbdefb0dee3d414c2257a50a08759f3d --- firmware/usb.c | 9 +++++++++ firmware/usbstack/usb_core.c | 22 +++++++++++++++------- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/firmware/usb.c b/firmware/usb.c index 3b73f8ec34..b5b556516c 100644 --- a/firmware/usb.c +++ b/firmware/usb.c @@ -431,6 +431,15 @@ static void NORETURN_ATTR usb_thread(void) #ifdef HAVE_USBSTACK case USB_NOTIFY_SET_ADDR: +#ifdef USB_DETECT_BY_REQUEST + /* first sign of a host that addresses before anything else; + * the class drivers must be on before interfaces are assigned */ + if(usb_state > USB_EXTRACTED) { + usb_state = USB_INSERTED; + usb_set_host_present(true); + } +#endif + /* intentional fallthrough */ case USB_NOTIFY_SET_CONFIG: case USB_NOTIFY_BUS_RESET: case USB_NOTIFY_CLASS_DRIVER: diff --git a/firmware/usbstack/usb_core.c b/firmware/usbstack/usb_core.c index acccee21d2..78becb0fc4 100644 --- a/firmware/usbstack/usb_core.c +++ b/firmware/usbstack/usb_core.c @@ -863,9 +863,22 @@ static void request_handler_device_get_descriptor(struct usb_ctrlrequest* req, u } } +static void usb_core_init_interfaces(void) +{ + if(usb_state == DEFAULT) { + set_serial_descriptor(); + usb_core_set_serial_function_id(); + allocate_interfaces_and_endpoints(); + } +} + static void usb_core_set_address(uint8_t address) { logf("usb_core: SET_ADR %d", address); + /* A host may address the device before making any other request; + * the driver answers SET_ADDRESS itself, so nothing has been + * allocated yet. */ + usb_core_init_interfaces(); usb_address = address; usb_state = ADDRESS; } @@ -1134,12 +1147,7 @@ static void usb_core_control_request_handler(struct usb_ctrlrequest* req, uint8_ usb_charging_maxcurrent_change(usb_charging_maxcurrent()); } #endif - if(usb_state == DEFAULT) { - set_serial_descriptor(); - usb_core_set_serial_function_id(); - - allocate_interfaces_and_endpoints(); - } + usb_core_init_interfaces(); switch(req->bRequestType & USB_RECIP_MASK) { case USB_RECIP_DEVICE: @@ -1251,7 +1259,7 @@ void usb_core_transfer_complete(int ep, int dir, int status, int length) { /* Control packet handling */ switch(dir | ep0_state) { - /* EXPECT_TX_DATA_STATUS_COMP -(status comp)-> EXPECT_TX_DATA_COMP -(data comp)-> READY + /* EXPECT_TX_DATA_STATUS_COMP -(status comp)-> EXPECT_TX_DATA_COMP -(data comp)-> READY * -(data comp)-> EXPECT_TX_STATUS_COMP -(status comp)-> READY */ case USB_DIR_OUT | EP0_EXPECT_TX_DATA_STATUS_COMP: logf("usb_core: control-in done success=%d", status == 0 && length == 0); From 9bcdd30293cb58fe65450aaa1a8b90a9e519eac8 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 14:18:06 +0200 Subject: [PATCH 247/408] usb_storage: flush storage when the host stops or ejects the unit A host that is done with a mass-storage device - "safely remove", eject, udisksctl power-off - sends START STOP UNIT with the start bit clear, and may cut power right after. usb_storage only marked the LUN ejected. Storage drivers can still hold data in RAM at that point: a flash translation layer keeps part-written pages until a later write completes them, and HAVE_STORAGE_FLUSH exists so they can be committed - but only shutdown and ROLO called it. On a device unplugged after a proper eject and then losing power, that data was lost although the host had done everything right. Call storage_flush() on stop and on eject, on targets that define HAVE_STORAGE_FLUSH. The SCSI handler runs in the USB thread, as do the reads and writes, so the flush cannot race them. Co-Authored-By: Claude Opus 5.5 Change-Id: I5b5811858c75dfb3ee89535ab59f4a19663945df --- firmware/usbstack/usb_storage.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/firmware/usbstack/usb_storage.c b/firmware/usbstack/usb_storage.c index 869a2dd3ed..2598a3d46d 100644 --- a/firmware/usbstack/usb_storage.c +++ b/firmware/usbstack/usb_storage.c @@ -1036,6 +1036,13 @@ static void handle_scsi_ready(struct command_block_wrapper* cbw) logf("scsi load/eject"); if((cbw->command_block[4] & 0x01) == 0) /* Don't start */ { +#ifdef HAVE_STORAGE_FLUSH + /* The host stops or ejects the unit when it is done with + * it - "safely remove" - and may cut power next. Commit + * what the storage driver still holds in RAM, such as a + * flash translation layer's part-written pages. */ + storage_flush(); +#endif if((cbw->command_block[4] & 0x02) != 0) /* eject */ { logf("scsi eject"); From 3c09d4570f9fa7ad1ce672fa707e096d2098b37b Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Mon, 28 Sep 2026 09:07:59 -0400 Subject: [PATCH 248/408] FS#14012 - Latvian Translation Update (Renalds Belaks) Change-Id: I904a8e5b688ff9b36220bf6631fa5369e4b3d025 --- apps/lang/latviesu.lang | 66 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/apps/lang/latviesu.lang b/apps/lang/latviesu.lang index ab4c8dccd8..f5dbfd7744 100644 --- a/apps/lang/latviesu.lang +++ b/apps/lang/latviesu.lang @@ -1006,10 +1006,10 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Pīķa filtrs %d" + *: "Pīķa filtrs" *: "Pīķa filtrs" @@ -17098,3 +17098,65 @@ *: "Iestatīt konteksta vienumu" + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Filtrs %d" + + + *: "Filtrs" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Filtra veids" + + + *: "Filtra veids" + + + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "Kompozītvideo izeja" + + + *: none + videoout: "Kompozītvideo izeja" + + + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "Skārienekrāna žestu saīsnes" + + + *: none + touchscreen: "Skārienekrāna žestu saīsnes" + + From 4dcb5bae9ddf188e3f2c12af5e10b6ea31c6275c Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 23 Sep 2026 23:44:02 +0200 Subject: [PATCH 249/408] rk27xx: fix busy-wait asm that modifies an input-only operand delay_nop() in lcd-rk27generic.c and udelay() in system-rk27xx.c both count a register down with subs, but pass it as an input operand only: asm volatile ("1: subs %[n], %[n], #1\n bne 1b" : : [n] "r" (cycles)); That tells GCC the register is unchanged afterwards, so it is free to load a constant once and reuse the register for every later call with the same argument. Current GCC does exactly that in lcd_display_init(): ldr r4, =10000 @ first delay_nop(10000) subs r4, r4, #1 @ ... counts r4 down to 0 bl lcd_write_reg subs r4, r4, #1 @ next delay_nop(10000): r4 not reloaded, @ 0 - 1 wraps, loop runs 2^32 times At 4 cycles per iteration and 200 MHz that is 85.9 s per wrapped call. Nine calls wrap, so lcd_init() took 9 x 85.9 = 773 s. Measured on a generic rk2705 with a tick timestamp either side of lcd_display_init(): 77320 ticks at HZ=100, i.e. 773.2 s. With this fix it is 9 ticks, clear loop included. That is why lcd_init() looked like a hang on current toolchains while it worked when the port was written. It also explains why no LCDC clock, divider, gating or strobe-timing change had any effect: the time was never spent in the LCD controller. udelay() happens to work today because its count is computed at run time on each call, but it has the same undefined behaviour and gets the same fix. Co-Authored-By: Claude Opus 5.5 Change-Id: Iecdd6cca0701c35bce3427f359a9a638b21291b5 --- .../arm/rk27xx/rk27generic/lcd-rk27generic.c | 45 +++++++------------ firmware/target/arm/rk27xx/system-rk27xx.c | 7 +-- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/firmware/target/arm/rk27xx/rk27generic/lcd-rk27generic.c b/firmware/target/arm/rk27xx/rk27generic/lcd-rk27generic.c index b40f2860d7..bb3998b60c 100644 --- a/firmware/target/arm/rk27xx/rk27generic/lcd-rk27generic.c +++ b/firmware/target/arm/rk27xx/rk27generic/lcd-rk27generic.c @@ -26,33 +26,22 @@ #include "spfd5420a.h" #include "lcdif-rk27xx.h" -/* TODO: convert to udelay() */ -static inline void delay_nop(int cycles) -{ - asm volatile ("1: subs %[n], %[n], #1 \n\t" - " bne 1b" - : - : [n] "r" (cycles)); -} - - -/* not tested */ static void lcd_sleep(bool sleep) { if (sleep) { /* enter sleep mode */ lcd_write_reg(DISPLAY_CTRL1, 0x0170); - delay_nop(50); + udelay(5); lcd_write_reg(DISPLAY_CTRL1, 0x0000); - delay_nop(50); + udelay(5); lcd_write_reg(PWR_CTRL1, 0x14B4); } else { /* return to normal operation */ lcd_write_reg(PWR_CTRL1, 0x14B0); - delay_nop(50); + udelay(5); lcd_write_reg(DISPLAY_CTRL1, 0x0173); } @@ -64,9 +53,9 @@ void lcd_display_init(void) unsigned int x, y; lcd_write_reg(RESET, 0x0001); - delay_nop(10000); + udelay(200); lcd_write_reg(RESET, 0x0000); - delay_nop(10000); + udelay(200); lcd_write_reg(IF_ENDIAN, 0x0000); /* order of receiving data */ lcd_write_reg(DRIVER_OUT_CTRL, 0x0000); lcd_write_reg(ENTRY_MODE, 0x1038); @@ -88,10 +77,10 @@ void lcd_display_init(void) lcd_write_reg(PART2_END, 0x0000); lcd_write_reg(PANEL_IF_CTRL1, 0x0011); - delay_nop(10000); + udelay(200); lcd_write_reg(PANEL_IF_CTRL2, 0x0202); lcd_write_reg(PANEL_IF_CTRL3, 0x0300); - delay_nop(10000); + udelay(200); lcd_write_reg(PANEL_IF_CTRL4, 0x021E); lcd_write_reg(PANEL_IF_CTRL5, 0x0202); lcd_write_reg(PANEL_IF_CTRL6, 0x0100); @@ -119,20 +108,20 @@ void lcd_display_init(void) lcd_write_reg(DISPLAY_CTRL1, 0x0001); lcd_write_reg(PWR_CTRL6, 0x0001); lcd_write_reg(PWR_CTRL7, 0x0060); - delay_nop(50000); + udelay(1000); lcd_write_reg(PWR_CTRL1, 0x16B0); - delay_nop(10000); + udelay(200); lcd_write_reg(PWR_CTRL2, 0x0147); - delay_nop(10000); + udelay(200); lcd_write_reg(PWR_CTRL3, 0x0117); - delay_nop(10000); + udelay(200); lcd_write_reg(PWR_CTRL4, 0x2F00); - delay_nop(50000); + udelay(1000); lcd_write_reg(VCOM_HV2, 0x0000); /* src 0x0090 */ - delay_nop(10000); + udelay(200); lcd_write_reg(VCOM_HV1, 0x0008); /* src 0x000A */ lcd_write_reg(PWR_CTRL3, 0x01BE); - delay_nop(10000); + udelay(200); /* addresses setup */ lcd_write_reg(WINDOW_H_START, 0x0000); @@ -144,11 +133,11 @@ void lcd_display_init(void) /* display on */ lcd_write_reg(DISPLAY_CTRL1, 0x0021); - delay_nop(40000); + udelay(800); lcd_write_reg(DISPLAY_CTRL1, 0x0061); - delay_nop(100000); + udelay(2000); lcd_write_reg(DISPLAY_CTRL1, 0x0173); - delay_nop(300000); + udelay(6000); /* clear screen */ diff --git a/firmware/target/arm/rk27xx/system-rk27xx.c b/firmware/target/arm/rk27xx/system-rk27xx.c index 6b88d86840..cf80e09509 100644 --- a/firmware/target/arm/rk27xx/system-rk27xx.c +++ b/firmware/target/arm/rk27xx/system-rk27xx.c @@ -185,13 +185,14 @@ void udelay(unsigned usecs) cycles_per_usec = (CPUFREQ_NORMAL + 999999) / 1000000; } - delay = (usecs * cycles_per_usec) / 5; + delay = (usecs * cycles_per_usec) / 4; asm volatile( "1: subs %0, %0, #1 \n" /* 1 cycle */ - " nop \n" /* 1 cycle */ " bne 1b \n" /* 3 cycles */ - : : "r"(delay) + : "+r"(delay) /* modified by the loop: in/out */ + : + : "cc" /* flags clobbered */ ); } From d3893ead3a2a75827deeb77eecffcc908796a8c2 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 20:24:05 +0200 Subject: [PATCH 250/408] rk27xx: keep interrupt and bulk endpoints in separate groups USB mass storage writes ran at 0.03 MB/s in the firmware - to the NAND and to the SD card alike - while the bootloader, with the same driver, wrote the same NAND at 2 MB/s. The firmware adds a HID interface, and without it writes ran at full speed. The UDC's endpoints come in groups of three - bulk OUT, bulk IN, interrupt IN: 1-3, 4-6 and so on - and allocation handed out the first free endpoint of each type, so HID's interrupt endpoint 3 landed in the group of mass storage's bulk endpoints 1 and 2. The host polls it every 16 ms, the idle endpoint NAKs, and each poll costs the group's bulk traffic: writes advanced about one packet per poll. Polled every 125 us instead, they all but stopped; moved to endpoint 6, in a group of its own, they ran at 2.36 MB/s, as without HID. Never give an interrupt endpoint a group with bulk endpoints in use, or the other way round, whichever class asks first. Since which endpoints are available then depends on what is already allocated, the driver tracks the allocation itself - option 2 of usb_drv.h, as usb-designware does - with its context in usb-rk27xx.h, which usb_core.c includes before usb_drv.h. HID keeps working. Tested on a generic rk2705 with ums_stress.py over USB mass storage to the SD card: 0.03 MB/s with HID on endpoint 3, 2.36 MB/s with it on endpoint 6 - the allocation this change produces for mass storage plus HID. (Measured with the driver's earlier allocator, before the USB core took allocation over; this carries the same rule into the new scheme.) Co-Authored-By: Claude Opus 5.5 Change-Id: Ib80ed206a3705f2c76fc8457b5c0a54d60a46402 --- firmware/export/usb-rk27xx.h | 37 +++++++++++++ firmware/target/arm/rk27xx/usb-drv-rk27xx.c | 58 ++++++++++++++++----- firmware/usbstack/usb_core.c | 2 + 3 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 firmware/export/usb-rk27xx.h diff --git a/firmware/export/usb-rk27xx.h b/firmware/export/usb-rk27xx.h new file mode 100644 index 0000000000..1d9a19518c --- /dev/null +++ b/firmware/export/usb-rk27xx.h @@ -0,0 +1,37 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef __USB_RK27XX_H__ +#define __USB_RK27XX_H__ + +#include +#include "cpu.h" /* USB_NUM_ENDPOINTS */ + +/* rk27xx UDC endpoint allocation: which endpoints may be allocated depends + * on what else is - interrupt and bulk endpoints never share a group of + * three - so the driver tracks the allocation itself (usb_drv.h, option 2; + * usb-drv-rk27xx.c). Include before usb_drv.h. */ +struct usb_drv_ep_alloc_ctx_rk27xx +{ + int8_t type[USB_NUM_ENDPOINTS][2]; + int max_packet_size[USB_NUM_ENDPOINTS][2]; +}; +#define usb_drv_ep_alloc_ctx usb_drv_ep_alloc_ctx_rk27xx + +#endif /* __USB_RK27XX_H__ */ diff --git a/firmware/target/arm/rk27xx/usb-drv-rk27xx.c b/firmware/target/arm/rk27xx/usb-drv-rk27xx.c index a1dda45ba0..fb1775f993 100644 --- a/firmware/target/arm/rk27xx/usb-drv-rk27xx.c +++ b/firmware/target/arm/rk27xx/usb-drv-rk27xx.c @@ -20,6 +20,7 @@ #include "config.h" #include "usb.h" +#include "usb-rk27xx.h" #include "usb_drv.h" #include "cpu.h" @@ -30,6 +31,7 @@ #include "usb_ch9.h" #include "usb_core.h" #include +#include #include "power.h" #define LOGF_ENABLE @@ -100,9 +102,6 @@ static struct endpoint_t endpoints[16] = ENDPOINT(15, INT, IN, &TX15STAT), /* IIN15 */ }; -struct usb_drv_ep_spec usb_drv_ep_specs[16]; /* filled in usb_drv_startup */ -uint8_t usb_drv_ep_specs_flags = 0; - static volatile bool set_address = false; static volatile bool set_configuration = false; @@ -273,16 +272,51 @@ static void udc_helper(void) } } -void usb_drv_startup(void) { - /* fill the endpoint spec table */ - usb_drv_ep_specs[0].type[DIR_OUT] = USB_ENDPOINT_XFER_CONTROL; - usb_drv_ep_specs[0].type[DIR_IN] = USB_ENDPOINT_XFER_CONTROL; - for(int ep_num = 1; ep_num < 16; ep_num++) { - int dir = endpoints[ep_num].dir; - int type = endpoints[ep_num].type; - usb_drv_ep_specs[ep_num].type[dir] = type; - usb_drv_ep_specs[ep_num].type[!dir] = USB_ENDPOINT_TYPE_NONE; +/* The UDC's endpoints come in groups of three - bulk OUT, bulk IN, + * interrupt IN: 1-3, 4-6 and so on - each endpoint with one fixed type and + * direction. An interrupt endpoint in the same group as bulk endpoints in + * use slows the bulk transfers down: every IN token the host polls it with + * and it NAKs costs the group's bulk traffic. With HID on endpoint 3 beside + * mass storage on 1 and 2, writes ran at 0.03 MB/s, and polled every 125 us + * instead of every 16 ms they all but stopped; on endpoint 6 they ran at + * 2.3 MB/s, as with no HID at all. So interrupt and bulk endpoints never + * share a group - which depends on what else is allocated, hence the + * driver's own allocator (usb_drv.h, option 2). */ +void usb_drv_ep_reset_alloc_ctx(struct usb_drv_ep_alloc_ctx* ctx) +{ + memset(ctx->type, -1, sizeof(ctx->type)); + memset(ctx->max_packet_size, 0, sizeof(ctx->max_packet_size)); +} + +bool usb_drv_ep_allocate(struct usb_drv_ep_alloc_ctx* ctx, int ep, int type, + int max_packet_size) +{ + int ep_num = EP_NUM(ep); + int dir = EP_DIR(ep); + struct endpoint_t *endp = &endpoints[ep_num]; + + if(ep_num == 0 || endp->type != type || + endp->dir != (dir == DIR_IN ? USB_DIR_IN : USB_DIR_OUT)) + return false; + + int first = (ep_num - 1) / 3 * 3 + 1; + for(int i = first; i < first + 3 && i < USB_NUM_ENDPOINTS; i++) + { + for(int d = 0; d < 2; d++) + { + if(ctx->type[i][d] != -1 && ctx->type[i][d] != type) + return false; + } } + + ctx->type[ep_num][dir] = type; + ctx->max_packet_size[ep_num][dir] = max_packet_size; + return true; +} + +/* one-time init: nothing to do, the endpoints are fixed in endpoints[] */ +void usb_drv_startup(void) +{ } /* return port speed FS=0, HS=1 */ diff --git a/firmware/usbstack/usb_core.c b/firmware/usbstack/usb_core.c index 78becb0fc4..f1b7359973 100644 --- a/firmware/usbstack/usb_core.c +++ b/firmware/usbstack/usb_core.c @@ -58,6 +58,8 @@ /* include order matters, include driver header before usb_drv.h */ #if CONFIG_USBOTG == USBOTG_DESIGNWARE #include "usb-designware.h" +#elif CONFIG_USBOTG == USBOTG_RK27XX +#include "usb-rk27xx.h" #endif #include "usb_drv.h" From c47b2e1be2669a96b03a41006787d716885b9acd Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Wed, 23 Sep 2026 20:45:02 -0400 Subject: [PATCH 251/408] opus: name the ARM inline-asm gates after the cores they cover Upstream's OPUS_ARM_INLINE_ASM means any ARM with inline assembly, with OPUS_ARM_INLINE_EDSP layered on top for ARMv5E. config.h instead defines exactly one of them per core, so the names read as broader than they are, and code added ARM_ARCH tests beside them to pin the scope down. Renamed to OPUS_ARM_ASM_ARMV4_ONLY and OPUS_ARM_ASM_ARMV5E_AND_LATER throughout celt and silk, upstream files included; README.rockbox records it for the next libopus sync. No code change: opus.elf disassembly and section sizes are identical before and after on ARMv4 (e200v1), ARMv5E (Clip+) and ARMv6 (iPod Nano 4G). Co-Authored-By: Claude Opus 5.5 Change-Id: I89932d348dedd76748a1bc7b3f2e7de1c5be49c8 --- lib/rbcodec/codecs/libopus/README.rockbox | 3 +++ lib/rbcodec/codecs/libopus/celt/_kiss_fft_guts.h | 4 ++-- lib/rbcodec/codecs/libopus/celt/arch.h | 4 ++-- lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h | 4 ++-- lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h | 4 ++-- lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h | 2 +- lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h | 2 +- lib/rbcodec/codecs/libopus/celt/arm/pfa_arm.h | 4 ++-- lib/rbcodec/codecs/libopus/celt/arm/pitch_arm.h | 2 +- lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h | 4 ++-- lib/rbcodec/codecs/libopus/celt/ecintrin.h | 4 ++-- lib/rbcodec/codecs/libopus/config.h | 6 ++++-- lib/rbcodec/codecs/libopus/silk/SigProc_FIX.h | 4 ++-- lib/rbcodec/codecs/libopus/silk/macros.h | 4 ++-- 14 files changed, 28 insertions(+), 23 deletions(-) diff --git a/lib/rbcodec/codecs/libopus/README.rockbox b/lib/rbcodec/codecs/libopus/README.rockbox index dd0ce70795..f32d2d007d 100644 --- a/lib/rbcodec/codecs/libopus/README.rockbox +++ b/lib/rbcodec/codecs/libopus/README.rockbox @@ -13,6 +13,9 @@ Opus: Celt: * changed #if FIXED_POINT to #ifdef FIXED_POINT in bands.c * changed #elif OPUS_ARM_INLINE_EDSP to #elif defined (OPUS_ARM_INLINE_EDSP) +* renamed OPUS_ARM_INLINE_ASM to OPUS_ARM_ASM_ARMV4_ONLY and + OPUS_ARM_INLINE_EDSP to OPUS_ARM_ASM_ARMV5E_AND_LATER throughout celt and + silk, since config.h defines exactly one of them per core * add #define ABS(a)(((a) < 0) ? - (a) :(a)) to mathops.h Opus-tools: diff --git a/lib/rbcodec/codecs/libopus/celt/_kiss_fft_guts.h b/lib/rbcodec/codecs/libopus/celt/_kiss_fft_guts.h index b2d2fdd7f3..f5794a4bff 100644 --- a/lib/rbcodec/codecs/libopus/celt/_kiss_fft_guts.h +++ b/lib/rbcodec/codecs/libopus/celt/_kiss_fft_guts.h @@ -90,11 +90,11 @@ do {(res).r = ADD32_ovflw((res).r,(a).r); (res).i = SUB32_ovflw((res).i,(a).i); \ }while(0) -#if defined(OPUS_ARM_INLINE_ASM) +#if defined(OPUS_ARM_ASM_ARMV4_ONLY) #include "arm/kiss_fft_armv4.h" #endif -#if defined(OPUS_ARM_INLINE_EDSP) +#if defined(OPUS_ARM_ASM_ARMV5E_AND_LATER) #include "arm/kiss_fft_armv5e.h" #endif #if defined(OPUS_CF_INLINE_ASM) diff --git a/lib/rbcodec/codecs/libopus/celt/arch.h b/lib/rbcodec/codecs/libopus/celt/arch.h index 39f01db5e5..5b35810016 100644 --- a/lib/rbcodec/codecs/libopus/celt/arch.h +++ b/lib/rbcodec/codecs/libopus/celt/arch.h @@ -173,9 +173,9 @@ static OPUS_INLINE opus_int16 SAT16(opus_int32 x) { #ifdef OPUS_ARM_PRESUME_AARCH64_NEON_INTR #include "arm/fixed_arm64.h" -#elif defined (OPUS_ARM_INLINE_EDSP) +#elif defined (OPUS_ARM_ASM_ARMV5E_AND_LATER) #include "arm/fixed_armv5e.h" -#elif defined (OPUS_ARM_INLINE_ASM) +#elif defined (OPUS_ARM_ASM_ARMV4_ONLY) #include "arm/fixed_armv4.h" #elif defined (BFIN_ASM) #include "fixed_bfin.h" diff --git a/lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h index 31dd2233af..3871b9785e 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/bands_arm.h @@ -35,14 +35,14 @@ #if defined(FIXED_POINT) && !defined(OPUS_ARM_NO_BANDS_ASM) -# if defined(OPUS_ARM_INLINE_ASM) && (ARM_ARCH == 4) +# if defined(OPUS_ARM_ASM_ARMV4_ONLY) && (ARM_ARCH == 4) # define OVERRIDE_DENORM_BAND void denorm_band_armv4(celt_sig *f, const celt_norm *x, int n, int g, int shift); # define DENORM_BAND denorm_band_armv4 -# elif defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH >= 5) +# elif defined(OPUS_ARM_ASM_ARMV5E_AND_LATER) && (ARM_ARCH >= 5) # define OVERRIDE_DENORM_BAND void denorm_band_armv5e(celt_sig *f, const celt_norm *x, int n, int g, diff --git a/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h index 67a0559047..04dfdea99a 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/comb_filter_arm.h @@ -38,7 +38,7 @@ #if defined(FIXED_POINT) && !defined(CUSTOM_MODES) \ && !defined(OPUS_ARM_NO_COMB_ASM) -# if defined(OPUS_ARM_INLINE_ASM) && (ARM_ARCH == 4) +# if defined(OPUS_ARM_ASM_ARMV4_ONLY) && (ARM_ARCH == 4) # define OVERRIDE_COMB_FILTER_CONST void comb_filter_const_armv4(opus_val32 *y, opus_val32 *x, int T, int N, @@ -61,7 +61,7 @@ void deemph_stereo_armv4(celt_sig *x0, celt_sig *x1, opus_val16 *pcm, int N, # define deemphasis_stereo_simple(in, pcm, N, coef0, mem) \ deemph_stereo_armv4((in)[0], (in)[1], (pcm), (N), (coef0), (mem)) -# elif defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH >= 5) +# elif defined(OPUS_ARM_ASM_ARMV5E_AND_LATER) && (ARM_ARCH >= 5) # define OVERRIDE_COMB_FILTER_CONST void comb_filter_const_armv5e(opus_val32 *y, opus_val32 *x, int T, int N, diff --git a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h index f99e700f7f..1088d5eac1 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv4.h @@ -34,7 +34,7 @@ Building with OPUS_ARM_NO_MDCT_ASM selects the C loops instead, which is how the two are compared. */ -#if defined(OPUS_ARM_INLINE_ASM) && defined(FIXED_POINT) \ +#if defined(OPUS_ARM_ASM_ARMV4_ONLY) && defined(FIXED_POINT) \ && !defined(OPUS_ARM_NO_MDCT_ASM) #define OVERRIDE_MDCT_PREROT diff --git a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h index 7b8aa7b4e1..e6b9e6c811 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/mdct_armv5e.h @@ -38,7 +38,7 @@ Building with OPUS_ARM_NO_MDCT_ASM selects the C loops instead, which is how the two are compared. */ -#if defined(OPUS_ARM_INLINE_EDSP) && defined(FIXED_POINT) \ +#if defined(OPUS_ARM_ASM_ARMV5E_AND_LATER) && defined(FIXED_POINT) \ && (ARM_ARCH >= 5) && !defined(OPUS_ARM_NO_MDCT_ASM) #define OVERRIDE_MDCT_PREROT diff --git a/lib/rbcodec/codecs/libopus/celt/arm/pfa_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/pfa_arm.h index d5910a3b86..afb21561e1 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/pfa_arm.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/pfa_arm.h @@ -11,13 +11,13 @@ #if defined(FIXED_POINT) && defined(OPUS_PFA) -#if defined(OPUS_ARM_INLINE_EDSP) +#if defined(OPUS_ARM_ASM_ARMV5E_AND_LATER) #define OVERRIDE_PFA_FFT15 void pfa_fft15_armv5e(const kiss_fft_cpx *in, kiss_fft_cpx *out, int ostride); #define PFA_FFT15(in, out, ostride) pfa_fft15_armv5e(in, out, ostride) -#elif defined(OPUS_ARM_INLINE_ASM) +#elif defined(OPUS_ARM_ASM_ARMV4_ONLY) #define OVERRIDE_PFA_FFT15 void pfa_fft15_armv4(const kiss_fft_cpx *in, kiss_fft_cpx *out, int ostride); diff --git a/lib/rbcodec/codecs/libopus/celt/arm/pitch_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/pitch_arm.h index e667abb10e..39679978f2 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/pitch_arm.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/pitch_arm.h @@ -74,7 +74,7 @@ extern void (*const DUAL_INNER_PROD_IMPL[OPUS_ARCHMASK+1])(const opus_val16 *x, 5.5 after. ARMv4 is deliberately left alone: without the packed multiplies the extraction would cost exactly what the saved loads buy. Escape hatch OPUS_ARM_NO_PITCH_ASM. */ -# if defined(OPUS_ARM_INLINE_EDSP) && defined(FIXED_POINT) \ +# if defined(OPUS_ARM_ASM_ARMV5E_AND_LATER) && defined(FIXED_POINT) \ && !defined(OVERRIDE_DUAL_INNER_PROD) && !defined(OPUS_ARM_NO_PITCH_ASM) void dual_inner_prod_armv5e(const opus_val16 *x, const opus_val16 *y01, const opus_val16 *y02, int N, opus_val32 *xy1, opus_val32 *xy2); diff --git a/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h b/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h index 6b725a3a47..98f00baee9 100644 --- a/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h +++ b/lib/rbcodec/codecs/libopus/celt/arm/vq_arm.h @@ -36,7 +36,7 @@ Shares OPUS_ARM_NO_BANDS_ASM with arm/bands_arm.h as the escape hatch. */ #if defined(FIXED_POINT) && !defined(OPUS_ARM_NO_BANDS_ASM) \ - && defined(OPUS_ARM_INLINE_EDSP) && (ARM_ARCH >= 5) + && defined(OPUS_ARM_ASM_ARMV5E_AND_LATER) && (ARM_ARCH >= 5) #define OVERRIDE_vq_exp_rotation1 void exp_rotation1_armv5e(celt_norm *X, int len, int stride, opus_val16 c, @@ -52,7 +52,7 @@ void normres_scale_armv5e(celt_norm *X, const int *iy, int N, int g, #define NORMRES_SCALE normres_scale_armv5e #elif defined(FIXED_POINT) && !defined(OPUS_ARM_NO_BANDS_ASM) \ - && defined(OPUS_ARM_INLINE_ASM) && (ARM_ARCH == 4) + && defined(OPUS_ARM_ASM_ARMV4_ONLY) && (ARM_ARCH == 4) #define OVERRIDE_vq_exp_rotation1 void exp_rotation1_armv4(celt_norm *X, int len, int stride, opus_val16 c, diff --git a/lib/rbcodec/codecs/libopus/celt/ecintrin.h b/lib/rbcodec/codecs/libopus/celt/ecintrin.h index 4336c4dec7..b16721b317 100644 --- a/lib/rbcodec/codecs/libopus/celt/ecintrin.h +++ b/lib/rbcodec/codecs/libopus/celt/ecintrin.h @@ -64,7 +64,7 @@ static __inline int ec_bsr(unsigned long _x){ # include "dsplib.h" # define EC_CLZ0 (31) # define EC_CLZ(_x) (_lnorm(_x)) -#elif defined(OPUS_ARM_INLINE_ASM) && !defined(OPUS_ARM_NO_ILOG_INLINE) +#elif defined(OPUS_ARM_ASM_ARMV4_ONLY) && !defined(OPUS_ARM_NO_ILOG_INLINE) /*ARMv4 has no CLZ instruction, so __builtin_clz compiles to a call to libgcc's __clzsi2. Leave EC_CLZ undefined and inline EC_ILOG below.*/ #elif __GNUC_PREREQ(3,4) @@ -83,7 +83,7 @@ static __inline int ec_bsr(unsigned long _x){ The majority of the time we can never pass it zero. When we need to, it can be special cased.*/ # define EC_ILOG(_x) (EC_CLZ0-EC_CLZ(_x)) -#elif defined(OPUS_ARM_INLINE_ASM) && !defined(OPUS_ARM_NO_ILOG_INLINE) +#elif defined(OPUS_ARM_ASM_ARMV4_ONLY) && !defined(OPUS_ARM_NO_ILOG_INLINE) /*Fifteen branchless instructions, no memory access. The libgcc call it replaces costs about half again once the call, its table load and the caller's register saves are counted. Compares against 0x10000, 0x100 and diff --git a/lib/rbcodec/codecs/libopus/config.h b/lib/rbcodec/codecs/libopus/config.h index 3b57bc4221..e3c9b0f299 100644 --- a/lib/rbcodec/codecs/libopus/config.h +++ b/lib/rbcodec/codecs/libopus/config.h @@ -78,10 +78,12 @@ #if defined(CPU_ARM) #define OPUS_ARM_ASM +/* Upstream's OPUS_ARM_INLINE_ASM and OPUS_ARM_INLINE_EDSP, renamed: upstream + layers EDSP on top of ASM, but here exactly one is defined per core. */ #if ARM_ARCH == 4 -#define OPUS_ARM_INLINE_ASM +#define OPUS_ARM_ASM_ARMV4_ONLY #elif ARM_ARCH > 4 -#define OPUS_ARM_INLINE_EDSP +#define OPUS_ARM_ASM_ARMV5E_AND_LATER #if (ARCH_PROFILE == ARM_PROFILE_MICRO) #define OPUS_ARM_NO_FFT_ASM #define OPUS_ARM_NO_MDCT_ASM diff --git a/lib/rbcodec/codecs/libopus/silk/SigProc_FIX.h b/lib/rbcodec/codecs/libopus/silk/SigProc_FIX.h index f9ae326326..a6685a99a5 100644 --- a/lib/rbcodec/codecs/libopus/silk/SigProc_FIX.h +++ b/lib/rbcodec/codecs/libopus/silk/SigProc_FIX.h @@ -621,11 +621,11 @@ static OPUS_INLINE opus_int64 silk_max_64(opus_int64 a, opus_int64 b) #include "MacroCount.h" #include "MacroDebug.h" -#ifdef OPUS_ARM_INLINE_ASM +#ifdef OPUS_ARM_ASM_ARMV4_ONLY #include "arm/SigProc_FIX_armv4.h" #endif -#ifdef OPUS_ARM_INLINE_EDSP +#ifdef OPUS_ARM_ASM_ARMV5E_AND_LATER #include "arm/SigProc_FIX_armv5e.h" #endif diff --git a/lib/rbcodec/codecs/libopus/silk/macros.h b/lib/rbcodec/codecs/libopus/silk/macros.h index f46e27caaa..870ba089c7 100644 --- a/lib/rbcodec/codecs/libopus/silk/macros.h +++ b/lib/rbcodec/codecs/libopus/silk/macros.h @@ -135,11 +135,11 @@ static OPUS_INLINE opus_int32 silk_CLZ32(opus_int32 in32) (*((Matrix_base_adr) + ((row)+(M)*(column)))) #endif -#ifdef OPUS_ARM_INLINE_ASM +#ifdef OPUS_ARM_ASM_ARMV4_ONLY #include "arm/macros_armv4.h" #endif -#ifdef OPUS_ARM_INLINE_EDSP +#ifdef OPUS_ARM_ASM_ARMV5E_AND_LATER #include "arm/macros_armv5e.h" #endif From e9a6b58870e799e508260d2b63f9b036dc2e2fd1 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Mon, 28 Sep 2026 12:55:21 -0400 Subject: [PATCH 252/408] toolchain: Fix hosted arm/mips toolchain build with texinfo 7.0/7.1 A crash bug in those versions of texinfo (fixed in 7.20) caused the glibc manual to fail to build. There is no build-time argument to disable the manual, but there is a version check that gracefully accomplishes the same thing. So, add 7.0/7.1 in the glibc configure script blacklist. Change-Id: If841aceeb7986db8274d145a5ea317278890eb30 --- tools/rockboxdev.sh | 4 ++-- tools/toolchain-patches/glibc-220-texinfo.patch | 12 ++++++++++++ tools/toolchain-patches/glibc-227-texinfo.patch | 12 ++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 tools/toolchain-patches/glibc-220-texinfo.patch create mode 100644 tools/toolchain-patches/glibc-227-texinfo.patch diff --git a/tools/rockboxdev.sh b/tools/rockboxdev.sh index 8b8d4d204a..d5b7bf2917 100755 --- a/tools/rockboxdev.sh +++ b/tools/rockboxdev.sh @@ -852,7 +852,7 @@ do # glibcopts="--enable-kernel=2.6.23 --enable-oldest-abi=2.4" build_linux_toolchain "arm-rockbox-linux-gnueabi" "2.38" "" "" "9.5.0" \ - "$gccopts" "2.6.32.71" "" "2.20" "$glibcopts" "glibc-220-make44.patch glibc-2.20-gcc10.patch" + "$gccopts" "2.6.32.71" "" "2.20" "$glibcopts" "glibc-220-make44.patch glibc-2.20-gcc10.patch glibc-220-texinfo.patch" # build alsa-lib # we need to set the prefix to how it is on device (/usr) and then # tweak install dir at make install step @@ -882,7 +882,7 @@ do glibcopts="--enable-kernel=3.2 --enable-oldest-abi=2.16 --disable-werror" # FIXME: maybe add -mhard-float? build_linux_toolchain "mipsel-rockbox-linux-gnu" "2.38" "" "binutils-c23.patch" "9.5.0" \ - "$gccopts" "3.10.108" "linux-c23.patch" "2.27" "$glibcopts" "glibc-227-make44.patch" + "$gccopts" "3.10.108" "linux-c23.patch" "2.27" "$glibcopts" "glibc-227-make44.patch glibc-227-texinfo.patch" # build alsa-lib # we need to set the prefix to how it is on device (/usr) and then # tweak install dir at make install step diff --git a/tools/toolchain-patches/glibc-220-texinfo.patch b/tools/toolchain-patches/glibc-220-texinfo.patch new file mode 100644 index 0000000000..cd2ab9e565 --- /dev/null +++ b/tools/toolchain-patches/glibc-220-texinfo.patch @@ -0,0 +1,12 @@ +diff -aur glibc-2.20/configure glibc-2.20-patch/configure +--- glibc-2.20/configure 2014-09-07 04:09:09.000000000 -0400 ++++ glibc-2.20-patch/configure 2026-09-28 12:33:47.509475820 -0400 +@@ -4851,7 +4851,7 @@ + ac_prog_version=`$MAKEINFO --version 2>&1 | sed -n 's/^.*GNU texinfo.* \([0-9][0-9.]*\).*$/\1/p'` + case $ac_prog_version in + '') ac_prog_version="v. ?.??, bad"; ac_verc_fail=yes;; +- 4.[5-9]*|4.[1-9][0-9]*|[5-9].*) ++ 4.[5-9]*|4.[1-9][0-9]*|[5-6].*|7.[2-9]*|[8-9].*) + ac_prog_version="$ac_prog_version, ok"; ac_verc_fail=no;; + *) ac_prog_version="$ac_prog_version, bad"; ac_verc_fail=yes;; + diff --git a/tools/toolchain-patches/glibc-227-texinfo.patch b/tools/toolchain-patches/glibc-227-texinfo.patch new file mode 100644 index 0000000000..597c9aeb2f --- /dev/null +++ b/tools/toolchain-patches/glibc-227-texinfo.patch @@ -0,0 +1,12 @@ +diff -aur glibc-2.27/configure glibc-2.27-patch/configure +--- glibc-2.27/configure 2018-02-01 11:17:18.000000000 -0500 ++++ glibc-2.27-patch/configure 2026-09-28 12:36:55.593733872 -0400 +@@ -4814,7 +4814,7 @@ + ac_prog_version=`$MAKEINFO --version 2>&1 | sed -n 's/^.*GNU texinfo.* \([0-9][0-9.]*\).*$/\1/p'` + case $ac_prog_version in + '') ac_prog_version="v. ?.??, bad"; ac_verc_fail=yes;; +- 4.[7-9]*|4.[1-9][0-9]*|[5-9].*) ++ 4.[5-9]*|4.[1-9][0-9]*|[5-6].*|7.[2-9]*|[8-9].*) + ac_prog_version="$ac_prog_version, ok"; ac_verc_fail=no;; + *) ac_prog_version="$ac_prog_version, bad"; ac_verc_fail=yes;; + From 8f38274e900afff60e4809922770d47dafe064c3 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Sat, 19 Sep 2026 16:06:49 -0400 Subject: [PATCH 253/408] warble: zero the mp3entry before reading metadata print_mp3entry() dereferences mb_track_id, but get_metadata() does not set every field of the uninitialized stack struct. For FLAC files this left garbage in the pointer and warble segfaulted in strlen about a third of the time, before decoding started. Clear the struct first. Co-Authored-By: Claude Sonnet 5 --- lib/rbcodec/test/warble.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/rbcodec/test/warble.c b/lib/rbcodec/test/warble.c index 10bc7485a9..4eab14762d 100644 --- a/lib/rbcodec/test/warble.c +++ b/lib/rbcodec/test/warble.c @@ -839,6 +839,9 @@ static void decode_file(const char *input_fn) /* Set up ci */ struct mp3entry id3; + /* get_metadata() doesn't set every field; don't leave stack garbage in the + pointers that print_mp3entry() dereferences */ + memset(&id3, 0, sizeof(id3)); if (!get_metadata(&id3, input_fd, input_fn)) { fprintf(stderr, "error: metadata parsing failed\n"); exit(1); From a81fd7b77fe6d0895bcbe14254a7a433af2c60e2 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Sat, 19 Sep 2026 16:07:28 -0400 Subject: [PATCH 254/408] flac: handle zero-width escaped rice partitions An escape code with a raw bit width of 0 means every residual in the partition is zero. get_sbits(&gb, 0) shifts by 32, which is undefined and returned stale cache bits instead of 0, so such streams decoded to garbage (FLAC decoder testbench file 64). Co-Authored-By: Claude Sonnet 5 --- lib/rbcodec/codecs/libffmpegFLAC/decoder.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/rbcodec/codecs/libffmpegFLAC/decoder.c b/lib/rbcodec/codecs/libffmpegFLAC/decoder.c index 7c3e0a4e96..dc5a98cf5b 100644 --- a/lib/rbcodec/codecs/libffmpegFLAC/decoder.c +++ b/lib/rbcodec/codecs/libffmpegFLAC/decoder.c @@ -161,8 +161,9 @@ static int decode_residuals(FLACContext *s, int32_t *decoded, int pred_order) tmp = get_bits(&gb, rice_bits); if (tmp == rice_esc) { tmp = get_bits(&gb, 5); + /* An escape width of 0 means all residuals are zero. */ for (; i < samples; i++) - *decoded++ = get_sbits(&gb, tmp); + *decoded++ = tmp ? get_sbits(&gb, tmp) : 0; } else { int real_limit = tmp ? (INT_MAX >> tmp) + 2 : INT_MAX; for (; i < samples; i++) { From 04ecb3c645c7c57976e7bd4cc096b78dbd52d106 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Sat, 19 Sep 2026 16:07:28 -0400 Subject: [PATCH 255/408] flac: don't divide by zero when STREAMINFO has no sample count A total sample count of 0 means unknown. It made the track length 0 and the bitrate estimate in flac_init() divided by it, crashing the codec (FLAC decoder testbench file 45). Report a bitrate of 0 in that case. Co-Authored-By: Claude Sonnet 5 --- lib/rbcodec/codecs/flac.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/rbcodec/codecs/flac.c b/lib/rbcodec/codecs/flac.c index 00d4dd71ca..dc3c4ba06f 100644 --- a/lib/rbcodec/codecs/flac.c +++ b/lib/rbcodec/codecs/flac.c @@ -209,8 +209,9 @@ static bool flac_init(FLACContext* fc, int first_frame_offset) } if (found_streaminfo) { - fc->bitrate = ((int64_t) (fc->filesize-fc->metadatalength) * 8) - / fc->length; + /* length is 0 when STREAMINFO has no total sample count */ + fc->bitrate = fc->length ? ((int64_t) (fc->filesize-fc->metadatalength) * 8) + / fc->length : 0; return true; } else { return false; From 89c38484eaf59340d857a3465dc29f178573c27d Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 28 Sep 2026 21:54:04 -0400 Subject: [PATCH 256/408] fix missing else in jpeg decoder. This didn't actually break anything since the error case wasn't handled anyway but not a good idea to leave alone. --- apps/plugins/imageviewer/jpeg/jpeg_decoder.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c index a06dc8ca1a..9726639084 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c +++ b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c @@ -922,7 +922,7 @@ void build_lut(struct jpeg* p_jpeg) p_jpeg->subsample_y[1] = 1; p_jpeg->subsample_y[2] = 1; } - if (p_jpeg->frameheader[0].horizontal_sampling == 1 + else if (p_jpeg->frameheader[0].horizontal_sampling == 1 && p_jpeg->frameheader[0].vertical_sampling == 2) { /* 4:2:2 vertically subsampled */ p_jpeg->store_pos[1] = 2; /* block positions are mirrored */ From 2bb498b96359c115477cedd15c81e6429699081c Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 28 Sep 2026 22:34:37 -0400 Subject: [PATCH 257/408] jpeg: fix decoding of grayscale images with 2x2 sampling A single-component (grayscale) scan is non-interleaved, so its MCU is one 8x8 block regardless of the sampling factors in the frame header (T.81 A.2.2). Some encoders write H=2,V=2 for the lone component, which sent both the imageviewer plugin decoder and the core loader down the 4:2:0 path: 6 blocks per 16x16 MCU, the image treated as colour, and the entropy data overrun. Force 1x1 sampling for single-component frames when parsing SOF0 so these images use the 4:4:4 layout with one block per MCU. Also add the missing else in fix_headers() in the core loader, matching the plugin. Tested on a Sansa e200 with both the plugin and the core loader, and the plugin in the e200 simulator and built for PC and for ARM (qemu). Fixes FS#13749. Co-Authored-By: Claude Opus 5.5 Change-Id: Iac2f925aab8cd602930470dca5da7dfb44e15961 --- apps/plugins/imageviewer/jpeg/jpeg_decoder.c | 7 +++++++ apps/recorder/jpeg_load.c | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c index 9726639084..00acb94b95 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c +++ b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c @@ -538,6 +538,13 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) return -3; /* Unsupported SOF0 subsampling */ } p_jpeg->blocks = n; + /* A single-component scan is non-interleaved: the MCU is one + 8x8 block regardless of the sampling factors (T.81 A.2.2) */ + if (n == 1) + { + p_jpeg->frameheader[0].horizontal_sampling = 1; + p_jpeg->frameheader[0].vertical_sampling = 1; + } } break; diff --git a/apps/recorder/jpeg_load.c b/apps/recorder/jpeg_load.c index c85d9965b4..6cf0a9640e 100644 --- a/apps/recorder/jpeg_load.c +++ b/apps/recorder/jpeg_load.c @@ -1049,6 +1049,13 @@ static int process_markers(struct jpeg* p_jpeg) return -3; /* Unsupported SOF0 subsampling */ } p_jpeg->blocks = n; + /* A single-component scan is non-interleaved: the MCU is one + 8x8 block regardless of the sampling factors (T.81 A.2.2) */ + if (n == 1) + { + p_jpeg->frameheader[0].horizontal_sampling = 1; + p_jpeg->frameheader[0].vertical_sampling = 1; + } } break; @@ -1476,7 +1483,7 @@ INLINE void fix_headers(struct jpeg* p_jpeg) p_jpeg->subsample_y[1] = 1; p_jpeg->subsample_y[2] = 1; } - if (p_jpeg->frameheader[0].horizontal_sampling == 1 + else if (p_jpeg->frameheader[0].horizontal_sampling == 1 && p_jpeg->frameheader[0].vertical_sampling == 2) { /* 4:2:2 vertically subsampled */ p_jpeg->store_pos[1] = 2; /* block positions are mirrored */ From 98f162b2125991b92443ccda78547b6b42c2de50 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 08:56:12 +0200 Subject: [PATCH 258/408] New target: Samsung YP-CP3 (rk27xx) Scaffolding for a port, starting from the rk27xx generic target: configure entry 146 (target id 117, model number 132), config/samsungypcp3.h, and firmware/target/arm/rk27xx/ypcp3/. - LCD: the rk27xx LCD interface (lcdif-rk27xx.c) with this panel's init sequence, from RE. See g#1050. Looks like SPFD5420A 18-bit bus, 400x240 landscape. It differs from the generic panel's sequence in the gamma curve and in not writing VCOM_HV1. - Backlight: PD4 / PWM0 with the generic board's timing, which the hwstub work found to be the same. - Storage: the microSD slot. Card detect is PC7, active low, as on the generic board. - Keys: the YP-R0's key set - five-way yog, Back, Menu, Rec/User, Power - so the target uses SAMSUNG_YPR0_PAD and its keymaps. Eight keys sit on two resistor ladders on the LSADC, found in the original firmware and measured on a unit: channel 1 up 158, down 295, select 435, left 561, right 687 channel 2 menu 155, back 292, user 431 each key owning half a step either side of its reading; power is GPIO C1, active high. Builds as firmware and as bootloader. Status 3 (unusable) in builds.pm. Co-Authored-By: Claude Opus 5.5 Change-Id: Iff4984023415f341ca7507af6b5b2b551201e953 --- bootloader/SOURCES | 2 +- firmware/SOURCES | 7 + firmware/export/config.h | 2 + firmware/export/config/samsungypcp3.h | 169 +++++++++++++++++ firmware/target/arm/rk27xx/backlight-rk27xx.c | 2 +- firmware/target/arm/rk27xx/debug-rk27xx.c | 4 +- firmware/target/arm/rk27xx/sd-rk27xx.c | 3 +- .../target/arm/rk27xx/ypcp3/button-target.h | 43 +++++ .../target/arm/rk27xx/ypcp3/button-ypcp3.c | 100 ++++++++++ firmware/target/arm/rk27xx/ypcp3/lcd-target.h | 25 +++ firmware/target/arm/rk27xx/ypcp3/lcd-ypcp3.c | 173 ++++++++++++++++++ .../target/arm/rk27xx/ypcp3/power-ypcp3.c | 51 ++++++ .../target/arm/rk27xx/ypcp3/powermgmt-ypcp3.c | 61 ++++++ tools/builds.pm | 4 + tools/configure | 81 +++++--- 15 files changed, 694 insertions(+), 33 deletions(-) create mode 100644 firmware/export/config/samsungypcp3.h create mode 100644 firmware/target/arm/rk27xx/ypcp3/button-target.h create mode 100644 firmware/target/arm/rk27xx/ypcp3/button-ypcp3.c create mode 100644 firmware/target/arm/rk27xx/ypcp3/lcd-target.h create mode 100644 firmware/target/arm/rk27xx/ypcp3/lcd-ypcp3.c create mode 100644 firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c create mode 100644 firmware/target/arm/rk27xx/ypcp3/powermgmt-ypcp3.c diff --git a/bootloader/SOURCES b/bootloader/SOURCES index 8510479739..a56a3999b8 100644 --- a/bootloader/SOURCES +++ b/bootloader/SOURCES @@ -60,7 +60,7 @@ mpio_hd200_hd300.c nwz_linux.c #elif defined(HIBY_LINUX) || defined(FIIO_M3K_LINUX) hibyos_linux.c -#elif defined(RK27_GENERIC) || defined(HM60X) || defined(HM801) \ +#elif defined(RK27_GENERIC) || defined(SAMSUNG_YPCP3) || defined(HM60X) || defined(HM801) \ || defined(MA9) || defined(MA9C) || defined(MA8) || defined(MA8C) \ || defined(IHIFI760) || defined(IHIFI960) || defined(IHIFI800) \ || defined(IHIFI770) || defined(IHIFI770C) diff --git a/firmware/SOURCES b/firmware/SOURCES index 39326ccb9a..f3995bc224 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1729,6 +1729,13 @@ target/arm/rk27xx/rk27generic/lcd-rk27generic.c target/arm/rk27xx/rk27generic/fmradio-i2c-rk27generic.c #endif +#if defined(SAMSUNG_YPCP3) +target/arm/rk27xx/ypcp3/button-ypcp3.c +target/arm/rk27xx/ypcp3/powermgmt-ypcp3.c +target/arm/rk27xx/ypcp3/power-ypcp3.c +target/arm/rk27xx/ypcp3/lcd-ypcp3.c +#endif + #if CONFIG_CPU == JZ4732 target/mips/ingenic_jz47xx/ata-nand-jz4740.c target/mips/ingenic_jz47xx/ata-sd-jz4740.c diff --git a/firmware/export/config.h b/firmware/export/config.h index c826aa513f..bb07f05026 100644 --- a/firmware/export/config.h +++ b/firmware/export/config.h @@ -483,6 +483,8 @@ Lyre prototype 1 */ #include "config/mpiohd300.h" #elif defined(RK27_GENERIC) #include "config/rk27generic.h" +#elif defined(SAMSUNG_YPCP3) +#include "config/samsungypcp3.h" #elif defined(HM60X) #include "config/hifimanhm60x.h" #elif defined(HM801) diff --git a/firmware/export/config/samsungypcp3.h b/firmware/export/config/samsungypcp3.h new file mode 100644 index 0000000000..7e206dd036 --- /dev/null +++ b/firmware/export/config/samsungypcp3.h @@ -0,0 +1,169 @@ +/* + * This config file is for the Samsung YP-CP3 (rk27xx) + * + * Bring-up state: LCD, backlight, NAND (Scheme A FTL), SD and USB are the + * rk27xx drivers, the LCD with this panel's init sequence; the keys are + * mapped. Power hold, battery measurement, the audio path and any tuner + * are not - see the TODOs in firmware/target/arm/rk27xx/ypcp3/. + */ + +/* For Rolo and boot loader */ +#define MODEL_NUMBER 132 + +#define MODEL_NAME "Samsung YP-CP3" + +/* define this if you have recording possibility */ +/* #define HAVE_RECORDING */ + +/* define the bitmask of hardware sample rates */ +#define HW_SAMPR_CAPS (SAMPR_CAP_44 | SAMPR_CAP_22 | SAMPR_CAP_11 \ + | SAMPR_CAP_48 | SAMPR_CAP_24 | SAMPR_CAP_12 \ + | SAMPR_CAP_32 | SAMPR_CAP_16 | SAMPR_CAP_8) + +/* define the bitmask of recording sample rates */ +#define REC_SAMPR_CAPS (SAMPR_CAP_44 | SAMPR_CAP_22 | SAMPR_CAP_11 \ + | SAMPR_CAP_48 | SAMPR_CAP_24 | SAMPR_CAP_12 \ + | SAMPR_CAP_32 | SAMPR_CAP_16 | SAMPR_CAP_8) + +/* define this if you have a colour LCD */ +#define HAVE_LCD_COLOR + +/* define this if you want album art for this target */ +#define HAVE_ALBUMART + +/* define this to enable bitmap scaling */ +#define HAVE_BMP_SCALING + +/* define this to enable JPEG decoding */ +#define HAVE_JPEG + +/* define this if you have access to the quickscreen */ +#define HAVE_QUICKSCREEN + +/* define this if you would like tagcache to build on this target */ +#define HAVE_TAGCACHE + +/* define this if you have a flash memory storage */ +#define HAVE_FLASH_STORAGE + +#define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) + +#define CONFIG_NAND NAND_RK27XX +#define HAVE_SW_TONE_CONTROLS + +/* commented for now */ +/* #define HAVE_HOTSWAP */ + +/* The NAND holds two volumes, SYS and USER; SYS is hidden - see + * config/rk27generic.h for why and for what HAVE_RK27XX_NAND_SYS does. + * storage.c numbers drives by driver, SD first: + * + * default HAVE_RK27XX_NAND_SYS + * drive 0 SD SD + * drive 1 NAND USER NAND SYS + * drive 2 NAND USER + */ +/* #define HAVE_RK27XX_NAND_SYS */ + +#ifdef HAVE_RK27XX_NAND_SYS +#define NUM_DRIVES 3 +#else +#define NUM_DRIVES 2 +#endif +#define SECTOR_SIZE 512 + +/* for small(ish) SD cards */ +#define HAVE_FAT16SUPPORT + +/* LCD dimensions */ +#define LCD_WIDTH 400 +#define LCD_HEIGHT 240 +/* sqrt(400^2 + 240^2) / 3.0 = 155.5 */ +#define LCD_DPI 155 +#define LCD_DEPTH 16 /* pseudo 262.144 colors */ +#define LCD_PIXELFORMAT RGB565 /* rgb565 */ + +/* Define this if your LCD can be put to sleep. HAVE_LCD_ENABLE + should be defined as well. */ +#ifndef BOOTLOADER +/* TODO: #define HAVE_LCD_SLEEP */ +/* TODO: #define HAVE_LCD_SLEEP_SETTING */ +#endif + +/* the YP-R0's key set - see ypcp3/button-target.h */ +#define CONFIG_KEYPAD SAMSUNG_YPR0_PAD + +/* Define this to enable morse code input */ +#define HAVE_MORSE_INPUT + +#define CONFIG_LCD LCD_SPFD5420A + +/* Define the type of audio codec */ +#define HAVE_RK27XX_CODEC + +/* Define this for LCD backlight available */ +#define HAVE_BACKLIGHT +#define HAVE_BACKLIGHT_BRIGHTNESS +#define MIN_BRIGHTNESS_SETTING 0 +#define MAX_BRIGHTNESS_SETTING 31 +#define DEFAULT_BRIGHTNESS_SETTING 20 +#define CONFIG_BACKLIGHT_FADING BACKLIGHT_FADING_SW_HW_REG + +/* Define this if you have a software controlled poweroff */ +#define HAVE_SW_POWEROFF + +/* The number of bytes reserved for loadable codecs */ +#define CODEC_SIZE 0x100000 + +/* The number of bytes reserved for loadable plugins */ +#define PLUGIN_BUFFER_SIZE 0x80000 + +/* TODO: Figure out real values */ +#define BATTERY_CAPACITY_DEFAULT 400 /* default battery capacity */ +#define BATTERY_CAPACITY_MIN 300 /* min. capacity selectable */ +#define BATTERY_CAPACITY_MAX 500 /* max. capacity selectable */ +#define BATTERY_CAPACITY_INC 10 /* capacity increment */ + +#define CONFIG_BATTERY_MEASURE VOLTAGE_MEASURE + +/* Hardware controlled charging with monitoring */ +#define CONFIG_CHARGING CHARGING_MONITOR + +/* define this if the unit can be powered or charged via USB */ +#define HAVE_USB_POWER + +/* USB On-the-go */ +#define CONFIG_USBOTG USBOTG_RK27XX + +/* enable these for the experimental usb stack */ +#define HAVE_USBSTACK + +#define USB_VENDOR_ID 0x071b +#define USB_PRODUCT_ID 0x3202 +#define HAVE_BOOTLOADER_USB_MODE + +/* The exact type of CPU */ +#define CONFIG_CPU RK27XX + +/* I2C interface */ +#define CONFIG_I2C I2C_RK27XX + +/* Define this to the CPU frequency */ +#define CPU_FREQ 200000000 + +/* Offset ( in the firmware file's header ) to the file CRC */ +#define FIRMWARE_OFFSET_FILE_CRC 0 + +/* Offset ( in the firmware file's header ) to the real data */ +#define FIRMWARE_OFFSET_FILE_DATA 8 + +/* Define this if you have adjustable CPU frequency */ +#define HAVE_ADJUSTABLE_CPU_FREQ + +/* Virtual LED (icon) */ +#define CONFIG_LED LED_VIRTUAL + +#define RKW_FORMAT +#define BOOTFILE_EXT "rkw" +#define BOOTFILE "rockbox." BOOTFILE_EXT +#define BOOTDIR "/.rockbox" diff --git a/firmware/target/arm/rk27xx/backlight-rk27xx.c b/firmware/target/arm/rk27xx/backlight-rk27xx.c index 6b7459a69e..f658f5f920 100644 --- a/firmware/target/arm/rk27xx/backlight-rk27xx.c +++ b/firmware/target/arm/rk27xx/backlight-rk27xx.c @@ -39,7 +39,7 @@ static int brightness = DEFAULT_BRIGHTNESS_SETTING; * So the function used to calculate the values in the matrix was: * f(x) = (PWM_FREQ_MAX - PWM_FREQ_MIN) * ((100*x/31 + 16)/116)^3 + PWM_FREQ_MIN */ -#ifdef RK27_GENERIC +#if defined(RK27_GENERIC) || defined(SAMSUNG_YPCP3) static const unsigned short lin_brightness[] = { 8110, 8191, 8304, 8455, 8649, 8892, 9189, 9545, 9966, 10457, 11024, 11671, 12406, 13232, 14156, 15182, diff --git a/firmware/target/arm/rk27xx/debug-rk27xx.c b/firmware/target/arm/rk27xx/debug-rk27xx.c index 28fd96da78..1204102f83 100644 --- a/firmware/target/arm/rk27xx/debug-rk27xx.c +++ b/firmware/target/arm/rk27xx/debug-rk27xx.c @@ -29,8 +29,10 @@ #include "storage.h" #include "lcd-target.h" -#ifdef RK27_GENERIC +#if defined(RK27_GENERIC) #define DEBUG_CANCEL BUTTON_VOL +#elif defined(SAMSUNG_YPCP3) +#define DEBUG_CANCEL BUTTON_BACK #elif defined(HM60X) || defined(HM801) || (CONFIG_KEYPAD == MA_PAD) || \ (CONFIG_KEYPAD == IHIFI_PAD) #define DEBUG_CANCEL BUTTON_LEFT diff --git a/firmware/target/arm/rk27xx/sd-rk27xx.c b/firmware/target/arm/rk27xx/sd-rk27xx.c index 4f9118a58d..a49e8e5c43 100644 --- a/firmware/target/arm/rk27xx/sd-rk27xx.c +++ b/firmware/target/arm/rk27xx/sd-rk27xx.c @@ -142,7 +142,8 @@ static void mmu_buff_reset(void) static inline bool card_detect_target(void) { -#if defined(RK27_GENERIC) || defined(IHIFI770) || defined(IHIFI770C) || defined(IHIFI800) +#if defined(RK27_GENERIC) || defined(IHIFI770) || defined(IHIFI770C) || defined(IHIFI800) || \ + defined(SAMSUNG_YPCP3) /* PC7, active low */ return !(GPIO_PCDR & 0x80); #elif defined(HM60X) || defined(HM801) diff --git a/firmware/target/arm/rk27xx/ypcp3/button-target.h b/firmware/target/arm/rk27xx/ypcp3/button-target.h new file mode 100644 index 0000000000..27cd5dae5b --- /dev/null +++ b/firmware/target/arm/rk27xx/ypcp3/button-target.h @@ -0,0 +1,43 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef _BUTTON_TARGET_H_ +#define _BUTTON_TARGET_H_ + +/* The YP-CP3 has the YP-R0's key set - a five-way pad, Back, Menu, the + * Rec/User key and Power - so it shares that keypad (SAMSUNG_YPR0_PAD) and + * its keymaps; the codes are the YP-R0's. */ +#define BUTTON_UP 0x00000001 +#define BUTTON_DOWN 0x00000002 +#define BUTTON_LEFT 0x00000004 +#define BUTTON_RIGHT 0x00000008 +#define BUTTON_USER 0x00000010 +#define BUTTON_MENU 0x00000020 +#define BUTTON_BACK 0x00000040 +#define BUTTON_POWER 0x00000080 +#define BUTTON_SELECT 0x00000100 + +#define BUTTON_MAIN (BUTTON_UP | BUTTON_DOWN | BUTTON_LEFT | \ + BUTTON_RIGHT | BUTTON_USER | BUTTON_MENU | \ + BUTTON_BACK | BUTTON_POWER | BUTTON_SELECT) + +#define POWEROFF_BUTTON BUTTON_POWER +#define POWEROFF_COUNT 30 + +#endif /* _BUTTON_TARGET_H_ */ diff --git a/firmware/target/arm/rk27xx/ypcp3/button-ypcp3.c b/firmware/target/arm/rk27xx/ypcp3/button-ypcp3.c new file mode 100644 index 0000000000..204081f2b3 --- /dev/null +++ b/firmware/target/arm/rk27xx/ypcp3/button-ypcp3.c @@ -0,0 +1,100 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "config.h" +#include "system.h" +#include "button.h" +#include "adc.h" + +/* Samsung YP-CP3 keys. + * + * Eight keys sit on two resistor ladders read by the LSADC, one key per + * ladder at a time; an idle ladder reads near full scale. Measured on a + * unit, the steps are about 133 counts apart: + * + * ADC_BUTTONS (1) up 158, down 295, select 435, left 561, right 687 + * ADC_EXTRA (2) menu 155, back 292, user (Rec) 431 + * + * Each key owns the range from half a step below its reading to half a step + * above. The original firmware ignores anything from 0x2ee (750) up, which + * is where the pad's range ends here too. + * + * Power is on its own pin, GPIO C1, active high. */ + +#define POWER_PIN (1 << 1) /* GPIO C1 */ + +struct ladder_key +{ + unsigned short below; /* upper bound, exclusive */ + int button; +}; + +static const struct ladder_key pad_keys[] = +{ + { 227, BUTTON_UP }, + { 365, BUTTON_DOWN }, + { 498, BUTTON_SELECT }, + { 624, BUTTON_LEFT }, + { 750, BUTTON_RIGHT }, +}; + +static const struct ladder_key side_keys[] = +{ + { 224, BUTTON_MENU }, + { 362, BUTTON_BACK }, + { 500, BUTTON_USER }, +}; + +static int ladder_button(int channel, const struct ladder_key *keys, int n) +{ + unsigned short val = adc_read(channel); + int button = 0; + int i; + + for (i = 0; i < n && button == 0; i++) + { + if (val < keys[i].below) + { + button = keys[i].button; + } + } + + return button; +} + +void button_init_device(void) +{ + GPIO_PCCON &= ~POWER_PIN; /* input */ +} + +int button_read_device(void) +{ + int buttons = 0; + + buttons |= ladder_button(ADC_BUTTONS, pad_keys, ARRAYLEN(pad_keys)); + buttons |= ladder_button(ADC_EXTRA, side_keys, ARRAYLEN(side_keys)); + + if (GPIO_PCDR & POWER_PIN) + { + buttons |= BUTTON_POWER; + } + + return buttons; +} diff --git a/firmware/target/arm/rk27xx/ypcp3/lcd-target.h b/firmware/target/arm/rk27xx/ypcp3/lcd-target.h new file mode 100644 index 0000000000..7879fa5010 --- /dev/null +++ b/firmware/target/arm/rk27xx/ypcp3/lcd-target.h @@ -0,0 +1,25 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef LCD_TARGET_H +#define LCD_TARGET_H + +#define LCD_DATABUS_WIDTH LCDIF_18BIT + +#endif diff --git a/firmware/target/arm/rk27xx/ypcp3/lcd-ypcp3.c b/firmware/target/arm/rk27xx/ypcp3/lcd-ypcp3.c new file mode 100644 index 0000000000..5b0adcc422 --- /dev/null +++ b/firmware/target/arm/rk27xx/ypcp3/lcd-ypcp3.c @@ -0,0 +1,173 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2014, 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* Samsung YP-CP3 LCD: a 240x400 panel with an SPFD5420A-compatible + * controller on the rk27xx LCD interface's 18-bit MCU bus, used in landscape. + * + * The init sequence is the one worked out over hwstub + * (utils/hwstub/tools/lua/samsungypcp3.lua). It differs from the rk27xx + * generic panel's in the gamma curve and in leaving VCOM_HV1 alone. The + * interface side - pins, LCDC, DMA, updates - is lcdif-rk27xx.c. */ + +#include "config.h" +#include "kernel.h" +#include "lcd.h" +#include "system.h" +#include "cpu.h" +#include "spfd5420a.h" +#include "lcdif-rk27xx.h" + +static void lcd_sleep(bool sleep) +{ + if (sleep) + { + lcd_write_reg(DISPLAY_CTRL1, 0x0170); + udelay(50); + lcd_write_reg(DISPLAY_CTRL1, 0x0000); + udelay(50); + lcd_write_reg(PWR_CTRL1, 0x14B4); + } + else + { + lcd_write_reg(PWR_CTRL1, 0x14B0); + udelay(50); + lcd_write_reg(DISPLAY_CTRL1, 0x0173); + } + + lcd_cmd(GRAM_WRITE); +} + +void lcd_display_init(void) +{ + int i; + + lcd_write_reg(RESET, 0x0001); + udelay(100000); + lcd_write_reg(RESET, 0x0000); + udelay(100000); + + lcd_write_reg(IF_ENDIAN, 0x0000); + lcd_write_reg(DRIVER_OUT_CTRL, 0x0000); + lcd_write_reg(WAVEFORM_CTRL, 0x0100); + lcd_write_reg(ENTRY_MODE, 0x1038); /* landscape */ + lcd_write_reg(SHAPENING_CTRL, 0x0000); + lcd_write_reg(DISPLAY_CTRL2, 0x0808); + lcd_write_reg(LOW_PWR_CTRL1, 0x0001); + lcd_write_reg(LOW_PWR_CTRL2, 0x0010); + lcd_write_reg(EXT_DISP_CTRL1, 0x0000); + lcd_write_reg(EXT_DISP_CTRL2, 0x0000); + lcd_write_reg(BASE_IMG_SIZE, 0x3100); + lcd_write_reg(BASE_IMG_CTRL, 0x0001); + lcd_write_reg(VSCROLL_CTRL, 0x0000); + lcd_write_reg(PART1_POS, 0x0000); + lcd_write_reg(PART1_START, 0x0000); + lcd_write_reg(PART1_END, 0x018F); + lcd_write_reg(PART2_POS, 0x0000); + lcd_write_reg(PART2_START, 0x0000); + lcd_write_reg(PART2_END, 0x0000); + + lcd_write_reg(PANEL_IF_CTRL1, 0x0011); + udelay(100); + lcd_write_reg(PANEL_IF_CTRL2, 0x0202); + lcd_write_reg(PANEL_IF_CTRL3, 0x0300); + udelay(100); + lcd_write_reg(PANEL_IF_CTRL4, 0x021E); + lcd_write_reg(PANEL_IF_CTRL5, 0x0202); + lcd_write_reg(PANEL_IF_CTRL6, 0x0100); + lcd_write_reg(FRAME_MKR_CTRL, 0x0000); + lcd_write_reg(MDDI_CTRL, 0x0000); + + lcd_write_reg(GAMMA_CTRL1, 0x0101); + lcd_write_reg(GAMMA_CTRL2, 0x0024); + lcd_write_reg(GAMMA_CTRL3, 0x1321); + lcd_write_reg(GAMMA_CTRL4, 0x2613); + lcd_write_reg(GAMMA_CTRL5, 0x2400); + lcd_write_reg(GAMMA_CTRL6, 0x0100); + lcd_write_reg(GAMMA_CTRL7, 0x1704); + lcd_write_reg(GAMMA_CTRL8, 0x0417); + lcd_write_reg(GAMMA_CTRL9, 0x0007); + lcd_write_reg(GAMMA_CTRL10, 0x0101); + lcd_write_reg(GAMMA_CTRL11, 0x0F05); + lcd_write_reg(GAMMA_CTRL12, 0x0F01); + lcd_write_reg(GAMMA_CTRL13, 0x010F); + lcd_write_reg(GAMMA_CTRL14, 0x050F); + lcd_write_reg(GAMMA_CTRL15, 0x0501); + lcd_write_reg(GAMMA_CTRL16, 0x0700); + + /* power on */ + lcd_write_reg(DISPLAY_CTRL1, 0x0001); + lcd_write_reg(PWR_CTRL6, 0x0001); + lcd_write_reg(PWR_CTRL7, 0x0060); + udelay(500); + lcd_write_reg(PWR_CTRL1, 0x16B0); + udelay(100); + lcd_write_reg(PWR_CTRL2, 0x0147); + udelay(100); + lcd_write_reg(PWR_CTRL3, 0x0117); + udelay(100); + lcd_write_reg(PWR_CTRL4, 0x2F00); + udelay(500); + lcd_write_reg(VCOM_HV2, 0x0000); + udelay(100); + lcd_write_reg(PWR_CTRL3, 0x01BE); + udelay(100); + + /* the whole panel */ + lcd_write_reg(WINDOW_H_START, 0x0000); + lcd_write_reg(WINDOW_H_END, 0x00EF); /* 239 */ + lcd_write_reg(WINDOW_V_START, 0x0000); + lcd_write_reg(WINDOW_V_END, 0x018F); /* 399 */ + lcd_write_reg(GRAM_H_ADDR, 0x0000); + lcd_write_reg(GRAM_V_ADDR, 0x0000); + + /* display on */ + lcd_write_reg(DISPLAY_CTRL1, 0x0021); + udelay(400); + lcd_write_reg(DISPLAY_CTRL1, 0x0061); + udelay(1000); + lcd_write_reg(DISPLAY_CTRL1, 0x0173); + udelay(3000); + + /* clear */ + lcd_cmd(GRAM_WRITE); + for (i = 0; i < LCD_WIDTH * LCD_HEIGHT; i++) + { + lcd_data(0x000000); + } + + lcd_sleep(false); +} + +void lcd_set_gram_area(int x_start, int y_start, int x_end, int y_end) +{ + lcdctrl_bypass(1); + LCDC_CTRL |= RGB24B; + + /* the panel is portrait: its horizontal is our vertical */ + lcd_write_reg(WINDOW_H_START, y_start); + lcd_write_reg(WINDOW_H_END, y_end); + lcd_write_reg(WINDOW_V_START, x_start); + lcd_write_reg(WINDOW_V_END, x_end); + lcd_write_reg(GRAM_H_ADDR, y_start); + lcd_write_reg(GRAM_V_ADDR, x_start); + + lcd_cmd(GRAM_WRITE); + LCDC_CTRL &= ~RGB24B; +} diff --git a/firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c b/firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c new file mode 100644 index 0000000000..12ad767a2d --- /dev/null +++ b/firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c @@ -0,0 +1,51 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include +#include "config.h" +#include "power.h" +#include "system.h" +#include "usb.h" + +/* TODO: the YP-CP3's power-hold line is not known yet. The rk27xx generic + * board holds power on PC0; driving a pin whose job on this board is unknown + * could do anything, so nothing is touched. Whatever the boot ROM or the + * original firmware left holding power stays holding it, and power_off() + * cannot switch the player off. */ +void power_init(void) +{ +} + +void power_off(void) +{ + disable_irq(); + while (1) + { + } +} + +unsigned int power_input_status(void) +{ + return (usb_detect() == USB_INSERTED) ? POWER_INPUT_MAIN_CHARGER : POWER_INPUT_NONE; +} + +bool charging_state(void) +{ + return usb_detect() == USB_INSERTED; +} diff --git a/firmware/target/arm/rk27xx/ypcp3/powermgmt-ypcp3.c b/firmware/target/arm/rk27xx/ypcp3/powermgmt-ypcp3.c new file mode 100644 index 0000000000..808fd3855a --- /dev/null +++ b/firmware/target/arm/rk27xx/ypcp3/powermgmt-ypcp3.c @@ -0,0 +1,61 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright © 2009 Bertrik Sikken + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "config.h" +#include "adc.h" +#include "adc-target.h" +#include "powermgmt.h" + +/* Battery voltage calculation and discharge/charge curves for the Samsung YP-CP3. + + TODO: copied from the rk27xx generic board - the YP-CP3's battery ADC + channel, divider and curves are not measured yet. + + Battery voltage is calculated under the assumption that the adc full-scale + readout represents 3.00V and that the battery ADC channel is fed with + exactly half of the battery voltage (through a resistive divider). + Discharge and charge curves have not been calibrated yet. +*/ + +unsigned short battery_level_disksafe = 3400; /* TODO: this is just an initial guess */ + +unsigned short battery_level_shutoff = 3300; /* TODO: this is just an initial guess */ + +/* voltages (millivolt) of 0%, 10%, ... 100% when charging disabled */ +unsigned short percent_to_volt_discharge[11] = +{ + /* TODO: simple uncalibrated curve, linear except for first 10% */ + 3300, 3600, 3665, 3730, 3795, 3860, 3925, 3990, 4055, 4120, 4185 +}; + +/* voltages (millivolt) of 0%, 10%, ... 100% when charging enabled */ +unsigned short percent_to_volt_charge[11] = + /* TODO: simple uncalibrated curve, linear except for first 10% */ + { 3300, 3600, 3665, 3730, 3795, 3860, 3925, 3990, 4055, 4120, 4185 }; + +/* full-scale ADC readout (2^10) in millivolt */ +#define BATTERY_SCALE_FACTOR 6000 + +/* Returns battery voltage from ADC [millivolts] */ +int _battery_voltage(void) +{ + return (adc_read(ADC_BATTERY) * BATTERY_SCALE_FACTOR) >> 10; +} + diff --git a/tools/builds.pm b/tools/builds.pm index 3adf0f2e38..eedac927ed 100644 --- a/tools/builds.pm +++ b/tools/builds.pm @@ -235,6 +235,10 @@ $releasenotes="/wiki/ReleaseNotes400"; name => 'Rockchip rk27xx', status => 1, }, + 'samsungypcp3' => { + name => 'Samsung YP-CP3', + status => 3, + }, 'samsungyh820' => { name => 'Samsung YH-820', status => 3, diff --git a/tools/configure b/tools/configure index 3e5331360b..34d497e5cc 100755 --- a/tools/configure +++ b/tools/configure @@ -1710,35 +1710,36 @@ cat < Date: Fri, 25 Sep 2026 15:43:35 +0200 Subject: [PATCH 259/408] rk27xx: only list PLL settings for the rates a target has set_codec_freq()'s table of codec PLL settings names every rate from 8 to 96 kHz by its HW_FREQ_ index, which exists only for rates in the target's HW_SAMPR_CAPS. Every CODEC_SLAVE target so far has all of them; a codec without 96 kHz at 256 fs - the WM8750 - leaves HW_FREQ_96 undefined and the table no longer builds. Wrap each entry in HW_HAVE_xx_(), as the codec drivers' tables do. For the existing targets the table is unchanged. Co-Authored-By: Claude Opus 5.5 Change-Id: Ifab688ffa6f83c5319d686fcf40fc2b887c75d24 --- firmware/target/arm/rk27xx/pcm-rk27xx.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/firmware/target/arm/rk27xx/pcm-rk27xx.c b/firmware/target/arm/rk27xx/pcm-rk27xx.c index 88139aecc4..df4b6d11b6 100644 --- a/firmware/target/arm/rk27xx/pcm-rk27xx.c +++ b/firmware/target/arm/rk27xx/pcm-rk27xx.c @@ -190,16 +190,16 @@ static void set_codec_freq(unsigned int freq) /* {CLKR, CLKF, CLKOD, CODECPLL_DIV} */ static const unsigned int pcm_freq_params[HW_NUM_FREQ][4] = { - [HW_FREQ_96] = {24, 255, 4, 1}, - [HW_FREQ_48] = {24, 127, 4, 1}, - [HW_FREQ_44] = {24, 293, 4, 4}, - [HW_FREQ_32] = {24, 127, 4, 2}, - [HW_FREQ_24] = {24, 127, 4, 3}, - [HW_FREQ_22] = {24, 146, 4, 4}, - [HW_FREQ_16] = {24, 127, 5, 4}, - [HW_FREQ_12] = {24, 127, 4, 7}, - [HW_FREQ_11] = {24, 146, 4, 9}, - [HW_FREQ_8] = {24, 127, 5, 9}, + HW_HAVE_96_([HW_FREQ_96] = {24, 255, 4, 1},) + HW_HAVE_48_([HW_FREQ_48] = {24, 127, 4, 1},) + HW_HAVE_44_([HW_FREQ_44] = {24, 293, 4, 4},) + HW_HAVE_32_([HW_FREQ_32] = {24, 127, 4, 2},) + HW_HAVE_24_([HW_FREQ_24] = {24, 127, 4, 3},) + HW_HAVE_22_([HW_FREQ_22] = {24, 146, 4, 4},) + HW_HAVE_16_([HW_FREQ_16] = {24, 127, 5, 4},) + HW_HAVE_12_([HW_FREQ_12] = {24, 127, 4, 7},) + HW_HAVE_11_([HW_FREQ_11] = {24, 146, 4, 9},) + HW_HAVE_8_([HW_FREQ_8] = {24, 127, 5, 9},) }; /* select divider output from codec pll */ SCU_DIVCON1 &= ~((1<<9) | (0xF<<5)); From 0dc08e53a962b8843ed04281274ce8939529a16d Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 15:43:35 +0200 Subject: [PATCH 260/408] S-35390A RTC: bring the driver back, handle 12-hour mode The S-35390A keeps the hour either as 0-23 or - its default after a power-on reset - as 0-11 plus a p.m. flag, selected by the 12/24 bit of status register 1. The driver assumed 24-hour mode and never set it: it masked the p.m. flag off, so on a chip left in 12-hour mode every afternoon read as morning, and writing an afternoon time stored an out-of-range hour. Read the mode in rtc_init() and convert the hour both ways. The chip's mode is left alone, as another firmware on the same player may depend on it - the Samsung YP-CP3's original firmware runs it in 12-hour mode and never touches the bit. If the status register cannot be read, the driver keeps assuming 24-hour mode, as before. Upstream removed the driver with its only users, the Meizu M3/M6 and Samsung YP-S3 ports (1a33d7990a); the Samsung YP-CP3 needs it, so it comes back here, with RTC_S35390A and its SOURCES entry. Also pick the I2C header by CONFIG_I2C, so rk27xx targets can use the driver; i2c_read()/i2c_write() have the same shape there. Co-Authored-By: Claude Opus 5.5 Change-Id: I54d8c8cfaa1b88237f6c4b08d2a510ec9fce8ec5 --- firmware/SOURCES | 2 + firmware/drivers/rtc/rtc_s35390a.c | 145 +++++++++++++++++++++++++++++ firmware/export/config.h | 1 + 3 files changed, 148 insertions(+) create mode 100644 firmware/drivers/rtc/rtc_s35390a.c diff --git a/firmware/SOURCES b/firmware/SOURCES index f3995bc224..2c8ff4bdbe 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -502,6 +502,8 @@ drivers/rtc/rtc_jz4740.c drivers/rtc/rtc_jz4760.c #elif (CONFIG_RTC == RTC_X1000) drivers/rtc/rtc_x1000.c +#elif (CONFIG_RTC == RTC_S35390A) +drivers/rtc/rtc_s35390a.c #elif (CONFIG_RTC == RTC_S35380A) drivers/rtc/rtc_s35380a.c #elif (CONFIG_RTC == RTC_D2) diff --git a/firmware/drivers/rtc/rtc_s35390a.c b/firmware/drivers/rtc/rtc_s35390a.c new file mode 100644 index 0000000000..5f12f90b87 --- /dev/null +++ b/firmware/drivers/rtc/rtc_s35390a.c @@ -0,0 +1,145 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2009 by Bertrik Sikken + * Copyright (C) 2008 by Robert Kukla + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include "config.h" +#include "rtc.h" +#if CONFIG_I2C == I2C_S5L8700 +#include "i2c-s5l8700.h" +#elif CONFIG_I2C == I2C_RK27XX +#include "i2c-rk27xx.h" +#endif +#include "timefuncs.h" + +/* Driver for the Seiko S35390A real-time clock chip with i2c interface + + This driver was derived from rtc_mr100.c and adapted for the S35390A + used in the Meizu M3 (and possibly others). + */ + +#define RTC_ADDR 0x60 + +#define STATUS_REG1 0 +#define STATUS_REG2 1 +#define REALTIME_DATA1 2 +#define REALTIME_DATA2 3 +#define INT1_REG 4 +#define INT2_REG 5 +#define CLOCK_CORR_REG 6 +#define FREE_REG 7 + +/* bits, once reversed into the usual order */ +#define STATUS1_24H (1 << 1) /* 1 = 24-hour, 0 = 12-hour (reset) */ +#define HOUR_PM (1 << 6) + +/* The chip keeps the hour as 0-23 or, the default after a power-on reset, + * as 0-11 plus the p.m. flag. The mode is left as found - another firmware + * on the same player may expect it - and the hour converted instead. */ +static bool mode_24h = true; + + +static void reverse_bits(unsigned char* v, int size) +{ + static const unsigned char flipnibble[] = + {0x00, 0x08, 0x04, 0x0C, 0x02, 0x0A, 0x06, 0x0E, + 0x01, 0x09, 0x05, 0x0D, 0x03, 0x0B, 0x07, 0x0F}; + + for (int i = 0; i < size; i++) { + v[i] = (flipnibble[v[i] & 0x0F] << 4) | + flipnibble[(v[i] >> 4) & 0x0F]; + } +} + +void rtc_init(void) +{ + unsigned char status; + + if (i2c_read(RTC_ADDR | (STATUS_REG1 << 1), -1, 1, &status) == 0) + { + reverse_bits(&status, 1); + mode_24h = (status & STATUS1_24H) != 0; + } +} + +int rtc_read_datetime(struct tm *tm) +{ + unsigned char buf[7]; + unsigned int i; + bool pm; + int ret; + + ret = i2c_read(RTC_ADDR | (REALTIME_DATA1 << 1), -1, sizeof(buf), buf); + reverse_bits(buf, sizeof(buf)); + + pm = (buf[4] & HOUR_PM) != 0; + buf[4] &= 0x3f; /* mask out p.m. flag */ + + for (i = 0; i < sizeof(buf); i++) + buf[i] = BCD2DEC(buf[i]); + + tm->tm_sec = buf[6]; + tm->tm_min = buf[5]; + tm->tm_hour = buf[4]; + if (!mode_24h && pm) + { + tm->tm_hour += 12; + } + tm->tm_mday = buf[2]; + tm->tm_mon = buf[1] - 1; + tm->tm_year = buf[0] + 100; + + set_day_of_week(tm); + set_day_of_year(tm); + + return ret; +} + +int rtc_write_datetime(const struct tm *tm) +{ + unsigned char buf[7]; + unsigned int i; + int ret; + + buf[6] = tm->tm_sec; + buf[5] = tm->tm_min; + buf[4] = tm->tm_hour; + buf[3] = tm->tm_wday; + buf[2] = tm->tm_mday; + buf[1] = tm->tm_mon + 1; + buf[0] = tm->tm_year - 100; + + if (!mode_24h) + { + buf[4] = tm->tm_hour % 12; + } + + for (i = 0; i < sizeof(buf); i++) + buf[i] = DEC2BCD(buf[i]); + + if (!mode_24h && tm->tm_hour >= 12) + { + buf[4] |= HOUR_PM; + } + + reverse_bits(buf, sizeof(buf)); + ret = i2c_write(RTC_ADDR | (REALTIME_DATA1 << 1), -1, sizeof(buf), buf); + + return ret; +} + diff --git a/firmware/export/config.h b/firmware/export/config.h index bb07f05026..3c4e164da1 100644 --- a/firmware/export/config.h +++ b/firmware/export/config.h @@ -339,6 +339,7 @@ Lyre prototype 1 */ #define RTC_MR100 12 #define RTC_MC13783 13 /* Freescale MC13783 PMIC */ #define RTC_S5L8700 14 +#define RTC_S35390A 15 #define RTC_JZ4740 16 /* Ingenic Jz4740 */ #define RTC_NANO2G 17 /* This seems to be a PCF5063x */ #define RTC_D2 18 /* Either PCF50606 or PCF50635 */ From 2190a31dac3ab2d1fe058b75e8b078a057e63b5e Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Tue, 29 Sep 2026 09:29:38 +0200 Subject: [PATCH 261/408] Samsung YP-CP3 define power hold pin Change-Id: I9fbfa00b37d3f2194df36f808fa01e2a2eab0e59 --- firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c b/firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c index 12ad767a2d..f171f61069 100644 --- a/firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c +++ b/firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c @@ -23,18 +23,22 @@ #include "system.h" #include "usb.h" -/* TODO: the YP-CP3's power-hold line is not known yet. The rk27xx generic - * board holds power on PC0; driving a pin whose job on this board is unknown - * could do anything, so nothing is touched. Whatever the boot ROM or the - * original firmware left holding power stays holding it, and power_off() - * cannot switch the player off. */ +/* Power is held on by GPIO C0, active high - as on the rk27xx generic + * board. The original firmware raises it at boot and, to switch off, + * drives it low and waits for the supply to go. */ +#define POWER_HOLD_PIN (1 << 0) /* GPIO C0 */ + void power_init(void) { + GPIO_PCDR |= POWER_HOLD_PIN; + GPIO_PCCON |= POWER_HOLD_PIN; /* output */ } void power_off(void) { disable_irq(); + GPIO_PCCON |= POWER_HOLD_PIN; + GPIO_PCDR &= ~POWER_HOLD_PIN; while (1) { } From f7655e226c735187e1847ce3dbcb3c652b9e5c3c Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Tue, 29 Sep 2026 09:30:23 +0200 Subject: [PATCH 262/408] Samsung YP-CP3: Seiko S-35390A on I2C as RTC Found in the original firmware by its command-in-address protocol and bit-reversed data; the original firmware leaves it in 12-hour mode, which the driver now handles. Change-Id: I9f4147d3057aaa6aa498a1750cb4e746d69b1be6 --- firmware/export/config/samsungypcp3.h | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/firmware/export/config/samsungypcp3.h b/firmware/export/config/samsungypcp3.h index 7e206dd036..d716ab2aca 100644 --- a/firmware/export/config/samsungypcp3.h +++ b/firmware/export/config/samsungypcp3.h @@ -1,10 +1,5 @@ /* * This config file is for the Samsung YP-CP3 (rk27xx) - * - * Bring-up state: LCD, backlight, NAND (Scheme A FTL), SD and USB are the - * rk27xx drivers, the LCD with this panel's init sequence; the keys are - * mapped. Power hold, battery measurement, the audio path and any tuner - * are not - see the TODOs in firmware/target/arm/rk27xx/ypcp3/. */ /* For Rolo and boot loader */ @@ -101,6 +96,10 @@ /* Define the type of audio codec */ #define HAVE_RK27XX_CODEC +/* Seiko S-35390A real-time clock on I2C, left in 12-hour mode by the + * original firmware */ +#define CONFIG_RTC RTC_S35390A + /* Define this for LCD backlight available */ #define HAVE_BACKLIGHT #define HAVE_BACKLIGHT_BRIGHTNESS From a35f344eef1a46948b9f329b30b285bb7340dfbc Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 15:43:35 +0200 Subject: [PATCH 263/408] YP-CP3: WM8750 audio The YP-CP3 uses Wolfson WM8750. Headphones are wired on OUT2 and a headphone amplifier enabled by GPIO F2, active high - all RE from the original firmware. The original firmware runs the codec as I2S master in its 12 MHz "USB mode", fed a fixed 12 MHz MCLK, which puts 44.1 kHz at 44.118. Rockbox instead makes the rk27xx the master and clocks the codec from the codec PLL at exactly 256 fs (CODEC_SLAVE, as every other rk27xx target with an external codec), so the codec's CLOCKING register is its normal-mode 256 fs setting at every rate. 96 kHz is left out: the WM8750 cannot take it at 256 fs. - config: HAVE_WM8750, CODEC_SLAVE, rates 8-48 kHz; the WM8750 has hardware tone controls, so HAVE_SW_TONE_CONTROLS goes - ypcp3/wmcodec-ypcp3.c: register writes over the rk27xx I2C driver - wm8751.c: on the YP-CP3, power on and drive OUT2 instead of OUT1, set the volume there, and switch the amplifier with the outputs - english.lang: the YP-CP3 gets the bass/treble cutoff settings the WM8750 brings Tested on a YP-CP3: playback at 44.1 and 48 kHz on headphones, pitch and volume correct. Co-Authored-By: Claude Opus 5.5 Change-Id: I870038fb6a21a9c26b025e3df7c220fd925f49f6 --- apps/lang/english.lang | 12 +++---- firmware/SOURCES | 3 ++ firmware/drivers/audio/wm8751.c | 19 ++++++++++ firmware/export/config/samsungypcp3.h | 14 ++++++-- .../target/arm/rk27xx/ypcp3/wmcodec-ypcp3.c | 36 +++++++++++++++++++ 5 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 firmware/target/arm/rk27xx/ypcp3/wmcodec-ypcp3.c diff --git a/apps/lang/english.lang b/apps/lang/english.lang index 4c2f0d58b0..112c4d9257 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -9284,15 +9284,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" @@ -9301,15 +9301,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" diff --git a/firmware/SOURCES b/firmware/SOURCES index 2c8ff4bdbe..ab8660d653 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1736,6 +1736,9 @@ target/arm/rk27xx/ypcp3/button-ypcp3.c target/arm/rk27xx/ypcp3/powermgmt-ypcp3.c target/arm/rk27xx/ypcp3/power-ypcp3.c target/arm/rk27xx/ypcp3/lcd-ypcp3.c +#ifndef BOOTLOADER +target/arm/rk27xx/ypcp3/wmcodec-ypcp3.c +#endif #endif #if CONFIG_CPU == JZ4732 diff --git a/firmware/drivers/audio/wm8751.c b/firmware/drivers/audio/wm8751.c index 9a688f6589..dcd1583fb7 100644 --- a/firmware/drivers/audio/wm8751.c +++ b/firmware/drivers/audio/wm8751.c @@ -187,6 +187,10 @@ void audiohw_preinit(void) and_l(~(1<<5), &GPIO1_OUT); or_l((1<<5), &GPIO1_ENABLE); or_l((1<<5), &GPIO1_FUNCTION); +#elif defined(SAMSUNG_YPCP3) + /* headphone amplifier enable, GPIO F2 active high: off for now */ + GPIO_PFDR &= ~(1<<2); + GPIO_PFCON |= (1<<2); #endif /* @@ -233,6 +237,9 @@ void audiohw_postinit(void) /* headphones + line-out */ wmcodec_set_bits(PWRMGMT2, PWRMGMT2_LOUT1 | PWRMGMT2_ROUT1 | PWRMGMT2_LOUT2 | PWRMGMT2_ROUT2); +#elif defined(SAMSUNG_YPCP3) + /* headphones, on OUT2 */ + wmcodec_set_bits(PWRMGMT2, PWRMGMT2_LOUT2 | PWRMGMT2_ROUT2); #else /* headphones */ wmcodec_set_bits(PWRMGMT2, PWRMGMT2_LOUT1 | PWRMGMT2_ROUT1); @@ -286,6 +293,8 @@ void audiohw_postinit(void) or_l((1<<25), &GPIO1_OUT); #elif defined(MPIO_HD300) or_l((1<<5), &GPIO1_OUT); +#elif defined(SAMSUNG_YPCP3) + GPIO_PFDR |= (1<<2); #endif } @@ -293,10 +302,18 @@ void audiohw_set_volume(int vol_l, int vol_r) { vol_l = vol_tenthdb2hw(vol_l); vol_r = vol_tenthdb2hw(vol_r); +#if defined(SAMSUNG_YPCP3) + /* headphones on OUT2 */ + wmcodec_set_masked(LOUT2, LOUT2_LOUT2VOL(vol_l), + LOUT2_LOUT2VOL_MASK); + wmcodec_set_masked(ROUT2, ROUT2_RO2VU | ROUT2_ROUT2VOL(vol_r), + ROUT2_ROUT2VOL_MASK); +#else wmcodec_set_masked(LOUT1, LOUT1_LOUT1VOL(vol_l), LOUT1_LOUT1VOL_MASK); wmcodec_set_masked(ROUT1, ROUT1_RO1VU | ROUT1_ROUT1VOL(vol_r), ROUT1_ROUT1VOL_MASK); +#endif } #ifdef AUDIOHW_HAVE_LINEOUT @@ -353,6 +370,8 @@ void audiohw_close(void) and_l(~(1<<25), &GPIO1_OUT); #elif defined(MPIO_HD300) and_l(~(1<<5), &GPIO1_OUT); +#elif defined(SAMSUNG_YPCP3) + GPIO_PFDR &= ~(1<<2); #endif /* 2. Disable all output buffers. */ diff --git a/firmware/export/config/samsungypcp3.h b/firmware/export/config/samsungypcp3.h index d716ab2aca..61f0925b13 100644 --- a/firmware/export/config/samsungypcp3.h +++ b/firmware/export/config/samsungypcp3.h @@ -44,7 +44,6 @@ #define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) #define CONFIG_NAND NAND_RK27XX -#define HAVE_SW_TONE_CONTROLS /* commented for now */ /* #define HAVE_HOTSWAP */ @@ -93,8 +92,17 @@ #define CONFIG_LCD LCD_SPFD5420A -/* Define the type of audio codec */ -#define HAVE_RK27XX_CODEC +/* Wolfson WM8750 on I2C, headphones on its OUT2. The codec is the I2S + * slave: the rk27xx drives the bus and clocks the codec from its codec PLL + * at 256 fs for every rate (pcm-rk27xx.c), so the codec's CLOCKING register + * is its normal-mode 256 fs setting throughout. The original firmware runs + * the codec as master off a fixed 12 MHz instead, in USB mode, which puts + * 44.1 kHz at 44.118. */ +#define HAVE_WM8750 +#define CODEC_SLAVE +#define CODEC_SRCTRL_11025HZ 0 +#define CODEC_SRCTRL_22050HZ 0 +#define CODEC_SRCTRL_44100HZ 0 /* Seiko S-35390A real-time clock on I2C, left in 12-hour mode by the * original firmware */ diff --git a/firmware/target/arm/rk27xx/ypcp3/wmcodec-ypcp3.c b/firmware/target/arm/rk27xx/ypcp3/wmcodec-ypcp3.c new file mode 100644 index 0000000000..16ba0bf0f2 --- /dev/null +++ b/firmware/target/arm/rk27xx/ypcp3/wmcodec-ypcp3.c @@ -0,0 +1,36 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "config.h" +#include "system.h" +#include "i2c-rk27xx.h" +#include "wmcodec.h" + +/* The WM8750's control interface in 2-wire mode, CSB low */ +#define WM8750_I2C_ADDR 0x34 + +/* A register write is two bytes: the 7-bit register number over bit 8 of + * the value, then the value's low byte. */ +void wmcodec_write(int reg, int data) +{ + unsigned char lo = data & 0xff; + + i2c_write(WM8750_I2C_ADDR, (reg << 1) | ((data >> 8) & 1), 1, &lo); +} From c922ef98c30194379abf997f65d3a675d0ad7bff Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 15:43:35 +0200 Subject: [PATCH 264/408] YP-CP3: Si4703 FM radio The YP-CP3's tuner is a Silicon Labs Si470x at I2C address 0x20, an Si4703 by the RDS the original firmware enables. The firmware drives it the Si470x way - writes from register 2, reads from 0x0A - starts its oscillator with TEST1 = 0x8100 and a 500 ms wait before ENABLE, and has no power or reset line for it. - config: CONFIG_TUNER SI4700 with RDS, polled (no interrupt line needed), and FM radio as an input source - si4700.c: the YP-CP3 starts the oscillator as the Sansas do - power-ypcp3.c: tuner power stubs - there is nothing to switch - the rk27xx tuner I2C glue, and "radio" in the target's configure entry - wm8751.c: audiohw_set_monitor() for a WM8750 target that plays the radio but cannot record (rk27xx has no recording yet). The tuner, on LINPUT1/RINPUT1, goes through the input PGA at +12 dB - as in the original firmware - into the output mixers, the DAC staying in the mix. The existing monitor routing lives in the recording code and switches OUT2 off, which is the YP-CP3's headphone output. Tested on a YP-CP3: stations tune and play at a sane level. Co-Authored-By: Claude Opus 5.5 Change-Id: Ic097fc85dbd7fb71fcc28c97a9d378fa09599e79 --- firmware/SOURCES | 3 ++ firmware/drivers/audio/wm8751.c | 37 +++++++++++++++++++ firmware/drivers/tuner/si4700.c | 2 +- firmware/export/config/samsungypcp3.h | 9 +++++ .../target/arm/rk27xx/ypcp3/power-ypcp3.c | 15 ++++++++ tools/configure | 2 +- 6 files changed, 66 insertions(+), 2 deletions(-) diff --git a/firmware/SOURCES b/firmware/SOURCES index ab8660d653..0664e5788a 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1739,6 +1739,9 @@ target/arm/rk27xx/ypcp3/lcd-ypcp3.c #ifndef BOOTLOADER target/arm/rk27xx/ypcp3/wmcodec-ypcp3.c #endif +#if CONFIG_TUNER +target/arm/rk27xx/rk27generic/fmradio-i2c-rk27generic.c +#endif #endif #if CONFIG_CPU == JZ4732 diff --git a/firmware/drivers/audio/wm8751.c b/firmware/drivers/audio/wm8751.c index dcd1583fb7..ecfa751cb2 100644 --- a/firmware/drivers/audio/wm8751.c +++ b/firmware/drivers/audio/wm8751.c @@ -427,6 +427,43 @@ void audiohw_set_depth_3d(int val) } #endif +#if defined(HAVE_WM8750) && defined(HAVE_FMRADIO_IN) && !defined(HAVE_RECORDING) +/* The FM tuner, on LINPUT1/RINPUT1, played through the analog bypass into + * the output mixers - for a target that plays the radio but cannot record + * it. As on the YP-CP3's original firmware, the signal goes through the + * input PGA at +12 dB, which the tuner's low line level needs. The DAC + * stays in the mix. */ +void audiohw_set_monitor(bool enable) +{ + if (enable) + { + wmcodec_set_masked(ADCL, ADCL_LINSEL_LINPUT1, ADCL_LINSEL_MASK); + wmcodec_set_masked(ADCR, ADCR_RINSEL_RINPUT1, ADCR_RINSEL_MASK); + wmcodec_set_reg(LINVOL, LINVOL_LIZC | LINVOL_LINVOL(0x27)); + wmcodec_set_reg(RINVOL, RINVOL_RIVU | RINVOL_RIZC | + RINVOL_RINVOL(0x27)); + wmcodec_set_bits(PWRMGMT1, PWRMGMT1_AINL | PWRMGMT1_AINR); + + wmcodec_set_masked(LEFTMIX1, LEFTMIX1_LMIXSEL_ADCLIN | + LEFTMIX1_LI2LOVOL(0x20), + 0x7 | LEFTMIX1_LI2LOVOL_MASK); + wmcodec_set_bits(LEFTMIX1, LEFTMIX1_LI2LO); + wmcodec_set_masked(RIGHTMIX1, RIGHTMIX1_RMIXSEL_ADCRIN, 0x7); + wmcodec_set_masked(RIGHTMIX2, RIGHTMIX2_RI2ROVOL(0x20), + RIGHTMIX2_RI2ROVOL_MASK); + wmcodec_set_bits(RIGHTMIX2, RIGHTMIX2_RI2RO); + } + else + { + wmcodec_clear_bits(LEFTMIX1, LEFTMIX1_LI2LO); + wmcodec_clear_bits(RIGHTMIX2, RIGHTMIX2_RI2RO); + wmcodec_set_bits(LINVOL, LINVOL_LINMUTE); + wmcodec_set_bits(RINVOL, RINVOL_RIVU | RINVOL_RINMUTE); + wmcodec_clear_bits(PWRMGMT1, PWRMGMT1_AINL | PWRMGMT1_AINR); + } +} +#endif + #ifdef HAVE_RECORDING #if 0 static void audiohw_set_ngat(int ngath, int type, bool enable) diff --git a/firmware/drivers/tuner/si4700.c b/firmware/drivers/tuner/si4700.c index 2767cbbfe8..ce53fdeceb 100644 --- a/firmware/drivers/tuner/si4700.c +++ b/firmware/drivers/tuner/si4700.c @@ -34,7 +34,7 @@ #include "backlight.h" #if defined(SANSA_CLIP) || defined(SANSA_E200V2) || defined(SANSA_FUZE) || defined(SANSA_C200V2) \ - || defined(SANSA_FUZEPLUS) + || defined(SANSA_FUZEPLUS) || defined(SAMSUNG_YPCP3) /* some models use the internal 32 kHz oscillator which needs special attention during initialisation, power-up and power-down. */ #define SI4700_USE_INTERNAL_OSCILLATOR diff --git a/firmware/export/config/samsungypcp3.h b/firmware/export/config/samsungypcp3.h index 61f0925b13..8597510f75 100644 --- a/firmware/export/config/samsungypcp3.h +++ b/firmware/export/config/samsungypcp3.h @@ -108,6 +108,15 @@ * original firmware */ #define CONFIG_RTC RTC_S35390A +/* Silicon Labs Si4703 FM tuner on I2C, with RDS. It has no power control - + * the original firmware has none either - and its audio reaches the codec + * on LINPUT1/RINPUT1. RDS is polled, which needs no interrupt line. */ +#define CONFIG_TUNER SI4700 +#define HAVE_RDS_CAP +#define CONFIG_RDS (RDS_CFG_POLL | RDS_CFG_PROCESS) +#define CONFIG_RDS_POLL_TICKS 4 +#define INPUT_SRC_CAPS SRC_CAP_FMRADIO + /* Define this for LCD backlight available */ #define HAVE_BACKLIGHT #define HAVE_BACKLIGHT_BRIGHTNESS diff --git a/firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c b/firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c index f171f61069..755d44c88a 100644 --- a/firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c +++ b/firmware/target/arm/rk27xx/ypcp3/power-ypcp3.c @@ -44,6 +44,21 @@ void power_off(void) } } +#if CONFIG_TUNER +/* The tuner has no power switch; the original firmware does not switch it + * either, it only puts the chip to sleep over I2C. */ +bool tuner_power(bool status) +{ + (void)status; + return true; +} + +bool tuner_powered(void) +{ + return true; +} +#endif + unsigned int power_input_status(void) { return (usb_detect() == USB_INSERTED) ? POWER_INPUT_MAIN_CHARGER : POWER_INPUT_NONE; diff --git a/tools/configure b/tools/configure index 34d497e5cc..cb55218684 100755 --- a/tools/configure +++ b/tools/configure @@ -3050,7 +3050,7 @@ fi bmp2rb_native="$rootdir/tools/bmp2rb -f 4" output="rockbox.rkw" bootoutput="bootloader.rkw" - appextra="recorder:gui" + appextra="recorder:gui:radio" plugins="" # toolset is the tools within the tools directory that we build for # this particular target. From a33b0c7360649daa2dca59172c89c9f610683745 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 16:16:27 +0200 Subject: [PATCH 265/408] rk27xx: recording over I2S and HDMA channel 1 pcm-rk27xx.c had no recording at all. Add it for targets with HAVE_RECORDING: HDMA channel 1 moves the I2S Rx FIFO into the recording buffer - hdreq 7, fixed source, incrementing destination, 32-bit inc8 slices, the mirror of the playback channel and the setup the Samsung YP-CP3's original firmware uses. Playback keeps channel 0, so the two can run together. - HDMA_ISR holds both channels' masks and flags; playback used to write all of it, clearing whatever the other channel had. Each channel now updates only its own bits, and INT_HDMA serves each channel's count down flag. Both channels start masked and clear. - Recording cannot mask the shared interrupt, so pcm_rec_lock() defers a completed buffer to pcm_rec_unlock() instead. - In master mode the I2S Rx side runs only while recording: started with nothing reading its FIFO it upsets playback. I2S_RXCTL gets the Tx frame format, plus bit 24 as the YP-CP3's original firmware sets it. - audio-rk27xx.c routes inputs through audiohw_set_recsrc() on targets that record. Only partly tested, on a YP-CP3: playback and FM radio still work after the interrupt changes, and the recording screen's peak meter follows the microphone, so samples arrive through the DMA. Not yet tested: playing back a recorded file, recording FM, long recordings, recording while playing. A known risk: a flag set by the hardware in the few cycles of the read-modify-write of HDMA_ISR would be lost and stall that channel. Other rk27xx targets build; their playback was not retested on hardware. Co-Authored-By: Claude Opus 5.5 Change-Id: Idc64bce8075d0fc628c29767fc33beda4fff4342 --- firmware/target/arm/rk27xx/audio-rk27xx.c | 5 + firmware/target/arm/rk27xx/pcm-rk27xx.c | 234 +++++++++++++++++++--- 2 files changed, 211 insertions(+), 28 deletions(-) diff --git a/firmware/target/arm/rk27xx/audio-rk27xx.c b/firmware/target/arm/rk27xx/audio-rk27xx.c index 6307274af2..231b87d883 100644 --- a/firmware/target/arm/rk27xx/audio-rk27xx.c +++ b/firmware/target/arm/rk27xx/audio-rk27xx.c @@ -29,10 +29,15 @@ static int output_source = AUDIO_SRC_PLAYBACK; static void select_audio_path(void) { +#ifdef HAVE_RECORDING + /* the codec routes the source for listening and recording both */ + audiohw_set_recsrc(input_source, input_flags & SRCF_RECORDING); +#else if(input_source == AUDIO_SRC_PLAYBACK) audiohw_set_monitor(false); else audiohw_set_monitor(true); +#endif } void audio_input_mux(int source, unsigned flags) diff --git a/firmware/target/arm/rk27xx/pcm-rk27xx.c b/firmware/target/arm/rk27xx/pcm-rk27xx.c index df4b6d11b6..f37eb194b0 100644 --- a/firmware/target/arm/rk27xx/pcm-rk27xx.c +++ b/firmware/target/arm/rk27xx/pcm-rk27xx.c @@ -31,6 +31,30 @@ static int locked = 0; +/* HDMA_ISR holds the interrupt masks (bits 8-13) and flags (bits 0-5) of + * both channels - channel 0 plays, channel 1 records - so each channel + * updates only its own bits. A flag is cleared by writing 0. */ +#define HDMA_CH0_ISR_BITS ((1<<12) | (1<<10) | (1<<9) | \ + (1<<4) | (1<<2) | (1<<0)) +#define HDMA_CH1_ISR_BITS ((1<<13) | (1<<11) | (1<<8) | \ + (1<<5) | (1<<3) | (1<<1)) +/* running: only the page count down interrupt unmasked, flags clear */ +#define HDMA_CH0_ISR_RUN ((1<<12) | (1<<9)) +#define HDMA_CH1_ISR_RUN ((1<<13) | (1<<8)) +/* stopped: all masked, flags clear */ +#define HDMA_CH0_ISR_STOP ((1<<12) | (1<<10) | (1<<9)) +#define HDMA_CH1_ISR_STOP ((1<<13) | (1<<11) | (1<<8)) +/* count down to zero */ +#define HDMA_CH0_DONE (1<<2) +#define HDMA_CH1_DONE (1<<3) + +static void hdma_isr_update(unsigned long bits, unsigned long val) +{ + int old = disable_irq_save(); + HDMA_ISR = (HDMA_ISR & ~bits) | val; + restore_irq(old); +} + /* Mask the DMA interrupt */ static void sink_lock(void) { @@ -56,7 +80,7 @@ static void sink_unlock(void) static void sink_dma_stop(void) { HDMA_CON0 = 0x00; - HDMA_ISR = 0x00; + hdma_isr_update(HDMA_CH0_ISR_BITS, HDMA_CH0_ISR_STOP); locked = 1; } @@ -74,18 +98,9 @@ static void hdma_i2s_transfer(const void *addr, size_t size) * (zero based) */ - HDMA_ISR = ((1<<13) | /* mask ch1 accumulation overflow irq */ - (1<<12) | /* mask ch0 accumulation overflow irq */ - (1<<11) | /* mask ch1 page count down irq */ - (0<<10) | /* UNMASK ch0 page count down irq */ - (1<<9) | /* mask ch0 transfer irq */ - (1<<8) | /* mask ch1 transfer irq */ - (0<<5) | /* clear ch1 accumulation overflow flag */ - (0<<4) | /* clear ch0 accumulation overflow flag */ - (0<<3) | /* clear ch1 count down to zero flag */ - (0<<2) | /* clear ch0 count down to zero flag */ - (0<<1) | /* clear ch1 active flag */ - (0<<0)); /* clear ch0 active flag */ + /* mask ch0 accumulation overflow and transfer irqs, unmask its page + * count down irq, clear its flags */ + hdma_isr_update(HDMA_CH0_ISR_BITS, HDMA_CH0_ISR_RUN); HDMA_ISCNT0 = 0x07; /* slice size in transfer size units (zero base) */ @@ -114,6 +129,18 @@ static void sink_dma_start(const void *addr, size_t size) hdma_i2s_transfer(addr, size); } +#define I2S_FORMAT ((1<<16) | /* LRCK/SCLK = 64 */ \ + (4<<8) | /* MCLK/SCLK = 4 */ \ + (1<<4) | /* 16bit samples */ \ + (0<<3) | /* stereo */ \ + (0<<1) | /* I2S IF */ \ + I2S_MASTER) +#ifdef CODEC_SLAVE +#define I2S_MASTER (1<<0) /* master mode */ +#else +#define I2S_MASTER (0<<0) /* slave mode */ +#endif + static void i2s_init(void) { #if defined(HAVE_RK27XX_CODEC) @@ -140,15 +167,10 @@ static void i2s_init(void) /* configure I2S module */ I2S_IER = 0; /* disable all i2s interrupts */ - I2S_TXCTL = (1<<16) | /* LRCK/SCLK = 64 */ - (4<<8) | /* MCLK/SCLK = 4 */ - (1<<4) | /* 16bit samples */ - (0<<3) | /* stereo */ - (0<<1) | /* I2S IF */ -#ifdef CODEC_SLAVE - (1<<0); /* master mode */ -#else - (0<<0); /* slave mode */ + I2S_TXCTL = I2S_FORMAT; +#ifdef HAVE_RECORDING + /* bit 24 as the original firmware of the Samsung YP-CP3 sets it */ + I2S_RXCTL = I2S_FORMAT | (1<<24); #endif /* the fifo is 16x32bits according to my tests @@ -231,6 +253,10 @@ static void sink_dma_init(void) INTC_IMR |= IRQ_ARM_HDMA; INTC_IECR |= IRQ_ARM_HDMA; + /* both channels stopped, with the HDMA clocked for the write */ + SCU_CLKCFG &= ~CLKCFG_HDMA; + HDMA_ISR = HDMA_CH0_ISR_STOP | HDMA_CH1_ISR_STOP; + audiohw_preinit(); i2s_init(); @@ -245,17 +271,46 @@ static void sink_set_freq(uint16_t freq) audiohw_set_frequency(freq); } +#ifdef HAVE_RECORDING +static void rec_dma_done(void); +static volatile int rec_locked = 0; +static volatile bool rec_pending = false; +#endif + /* audio DMA ISR called when chunk from callers buffer has been transfered */ void INT_HDMA(void) { - const void *start; - size_t size; + unsigned long isr = HDMA_ISR; - if (pcm_play_dma_complete_callback(PCM_DMAST_OK, &start, &size)) + if (isr & HDMA_CH0_DONE) { - hdma_i2s_transfer(start, size); - pcm_play_dma_status_callback(PCM_DMAST_STARTED); + const void *start; + size_t size; + + hdma_isr_update(HDMA_CH0_DONE, 0); + + if (pcm_play_dma_complete_callback(PCM_DMAST_OK, &start, &size)) + { + hdma_i2s_transfer(start, size); + pcm_play_dma_status_callback(PCM_DMAST_STARTED); + } } + +#ifdef HAVE_RECORDING + if (isr & HDMA_CH1_DONE) + { + hdma_isr_update(HDMA_CH1_DONE, 0); + + if (rec_locked) + { + rec_pending = true; + } + else + { + rec_dma_done(); + } + } +#endif } struct pcm_sink builtin_pcm_sink = { @@ -279,4 +334,127 @@ struct pcm_sink builtin_pcm_sink = { /**************************************************************************** ** Recording DMA transfer **/ -/* TODO */ +#ifdef HAVE_RECORDING +/* HDMA channel 1 from the I2S Rx FIFO. Playback and recording share the + * HDMA interrupt, so locking recording cannot mask it; a buffer completed + * while locked is handed over at unlock instead. */ + +/* I2S_OPR while playing only, and while recording too. In master mode the + * Rx side runs only while recording: started with nothing reading its FIFO + * it upsets playback. */ +#ifdef CODEC_SLAVE +#define I2S_OPR_START_TX (1<<1) +#define I2S_OPR_START_RX (1<<0) +#else +#define I2S_OPR_START_TX 0 /* not used in slave mode */ +#define I2S_OPR_START_RX 0 +#endif +#define I2S_OPR_PLAY ((0<<6) | /* HDMA Req1 enable */ \ + (1<<5) | /* HDMA Req2 disable */ \ + (0<<4) | /* Req1 for Tx fifo */ \ + (1<<3) | /* Req2 for Rx fifo */ \ + I2S_OPR_START_TX) +#define I2S_OPR_RECORD ((I2S_OPR_PLAY & ~(1<<5)) | I2S_OPR_START_RX) + +static void hdma_i2s_rec_transfer(void *addr, size_t size) +{ + SCU_CLKCFG &= ~CLKCFG_HDMA; /* enable HDMA clock */ + + /* the DMA writes the buffer: no line of it may be written back over + * the samples later */ + commit_discard_dcache_range(addr, size); + + HDMA_ISRC1 = (uint32_t)&I2S_RXR; /* i2s rx fifo */ + HDMA_IDST1 = (uint32_t)addr; /* destination address */ + HDMA_ICNT1 = (uint16_t)((size>>2) - 1); /* number of dma transactions + * of transfer size bytes + * (zero based) + */ + + hdma_isr_update(HDMA_CH1_ISR_BITS, HDMA_CH1_ISR_RUN); + + HDMA_ISCNT1 = 0x07; /* slice size in transfer size units (zero base) */ + + HDMA_IPNCNTD1 = 0x01; /* page count */ + + HDMA_CON1 = ((0<<23) | /* page mode */ + (1<<22) | /* slice mode */ + (1<<21) | /* DMA enable */ + (1<<18) | /* generate interrupt */ + (0<<16) | /* on-the-fly is not supported by rk27xx */ + (5<<13) | /* transfer mode inc8 */ + (7<<9) | /* external hdreq from i2s rx */ + (1<<7) | /* fixed source address */ + (0<<5) | /* increment destination address */ + (2<<3) | /* transfer size = 32bits word */ + (0<<1) | /* command of software DMA (not relevant) */ + (1<<0)); /* hardware trigger DMA mode */ +} + +static void rec_dma_done(void) +{ + void *start; + size_t size; + + if (pcm_rec_dma_complete_callback(PCM_DMAST_OK, &start, &size)) + { + hdma_i2s_rec_transfer(start, size); + pcm_rec_dma_status_callback(PCM_DMAST_STARTED); + } +} + +void pcm_rec_lock(void) +{ + int old = disable_irq_save(); + ++rec_locked; + restore_irq(old); +} + +void pcm_rec_unlock(void) +{ + int old = disable_irq_save(); + + if (--rec_locked == 0 && rec_pending) + { + rec_pending = false; + rec_dma_done(); + } + + restore_irq(old); +} + +void pcm_rec_dma_stop(void) +{ + HDMA_CON1 = 0x00; + hdma_isr_update(HDMA_CH1_ISR_BITS, HDMA_CH1_ISR_STOP); + rec_pending = false; + + I2S_OPR = I2S_OPR_PLAY; +} + +void pcm_rec_dma_start(void *addr, size_t size) +{ + pcm_rec_dma_stop(); + + I2S_OPR = I2S_OPR_PLAY | (1<<16); /* reset Rx */ + I2S_OPR = I2S_OPR_RECORD; + + hdma_i2s_rec_transfer(addr, size); +} + +void pcm_rec_dma_init(void) +{ + pcm_rec_dma_stop(); +} + +void pcm_rec_dma_close(void) +{ + pcm_rec_dma_stop(); +} + +const void * pcm_rec_dma_get_peak_buffer(void) +{ + /* where the DMA writes now */ + return (const void *)(HDMA_CDST1 & ~3); +} +#endif /* HAVE_RECORDING */ From 3fbf975ecc2b5ee28da9101b13ad0adeee8a0edc Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 16:16:27 +0200 Subject: [PATCH 266/408] YP-R0 keypad: recording screen and FM recording keys The keymap had no recording screen context, so on the recording screen no key produced ACTION_REC_PAUSE and recording could not be started; nor did the FM screen have a record action. - recording screen: User starts and pauses, held opens a new file; left/right set the gain of the selected line; Menu opens the settings - FM screen: User records the radio (FM_RECORD enabled for this keypad) User is the Rec key on the Samsung YP-CP3, which shares this keypad. Tested only in a YP-CP3 build, not yet on hardware; the YP-R0 itself was not built. Co-Authored-By: Claude Opus 5.5 Change-Id: I655865d0dcf3e72da4e2d1cba24bc296919ec261 --- apps/keymaps/keymap-ypr0.c | 23 +++++++++++++++++++++++ apps/radio/radio.c | 1 + 2 files changed, 24 insertions(+) diff --git a/apps/keymaps/keymap-ypr0.c b/apps/keymaps/keymap-ypr0.c index 099bc5a338..e62ff652fd 100644 --- a/apps/keymaps/keymap-ypr0.c +++ b/apps/keymaps/keymap-ypr0.c @@ -239,6 +239,9 @@ static const struct button_mapping button_context_radio[] = { { ACTION_FM_PLAY, BUTTON_SELECT | BUTTON_REL, BUTTON_SELECT }, { ACTION_FM_NEXT_PRESET, BUTTON_USER | BUTTON_RIGHT, BUTTON_NONE }, { ACTION_FM_PREV_PRESET, BUTTON_USER | BUTTON_LEFT, BUTTON_NONE }, +#ifdef HAVE_RECORDING + { ACTION_FM_RECORD, BUTTON_USER | BUTTON_REL, BUTTON_USER }, +#endif /* Volume */ { ACTION_SETTINGS_INC, BUTTON_UP | BUTTON_REPEAT, BUTTON_NONE }, @@ -252,6 +255,22 @@ static const struct button_mapping button_context_radio[] = { { ACTION_STD_NEXTREPEAT, BUTTON_RIGHT | BUTTON_REPEAT, BUTTON_NONE }, }; /* button_context_radio */ +#ifdef HAVE_RECORDING +/* User - the Rec key on the YP-CP3 - starts and pauses, held opens a new + * file; left/right set the gain of the selected line */ +static const struct button_mapping button_context_recscreen[] = { + { ACTION_REC_PAUSE, BUTTON_USER|BUTTON_REL, BUTTON_USER }, + { ACTION_REC_NEWFILE, BUTTON_USER|BUTTON_REPEAT, BUTTON_USER }, + { ACTION_STD_MENU, BUTTON_MENU|BUTTON_REL, BUTTON_MENU }, + { ACTION_SETTINGS_INC, BUTTON_RIGHT, BUTTON_NONE }, + { ACTION_SETTINGS_INCREPEAT, BUTTON_RIGHT|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_SETTINGS_DEC, BUTTON_LEFT, BUTTON_NONE }, + { ACTION_SETTINGS_DECREPEAT, BUTTON_LEFT|BUTTON_REPEAT, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD) +}; /* button_context_recscreen */ +#endif + const struct button_mapping* get_context_mapping(int context) { switch (context & ~CONTEXT_LOCKED) @@ -296,6 +315,10 @@ const struct button_mapping* get_context_mapping(int context) return button_context_pitchscreen; case CONTEXT_KEYBOARD: return button_context_keyboard; +#ifdef HAVE_RECORDING + case CONTEXT_RECSCREEN: + return button_context_recscreen; +#endif } return button_context_standard; } diff --git a/apps/radio/radio.c b/apps/radio/radio.c index a9b53971b2..03639258c1 100644 --- a/apps/radio/radio.c +++ b/apps/radio/radio.c @@ -106,6 +106,7 @@ #define FM_PLAY #define FM_PREV_PRESET #define FM_NEXT_PRESET +#define FM_RECORD #elif (CONFIG_KEYPAD == SANSA_FUZEPLUS_PAD) #define FM_PRESET_ADD From 3689292b2873f534c6c13c8706a7200c9a03a881 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 16:16:27 +0200 Subject: [PATCH 267/408] YP-CP3: recording from the microphone and FM radio - config: HAVE_RECORDING, sources microphone and FM, 8-48 kHz - wm8751.c: the YP-CP3's own audiohw_set_recsrc(). The rk27xx cannot send received samples straight back out, so what is heard of an input goes through the codec's analog bypass - the input PGA into the output mixers: the radio always, the microphone never. As in the original firmware, the radio passes the PGA at +12 dB when only listened to, and the microphone - mono, on RINPUT2 - gets +13 dB boost and is recorded by the right ADC onto both channels (the original firmware's noise gate is left out). The HD300's version assumes the microphone on INPUT3 and headphones on OUT1, and switches OUT2 - the YP-CP3's headphones - off. - wm8751.c: the playback-only FM monitor added for the YP-CP3 goes; the radio is routed by audiohw_set_recsrc() now, as on the HD300 - wm8751.h: DATSEL, which ADC feeds each channel of the output data Only partly tested on hardware: FM radio still plays, and the recording screen's peak meter follows the microphone. Not yet tested: a recorded file played back, recording FM, recording gain range. Co-Authored-By: Claude Opus 5.5 Change-Id: I3c25f4333f765d98dddc2fd722c111f8493bbe77 --- firmware/drivers/audio/wm8751.c | 102 ++++++++++++++++---------- firmware/export/config/samsungypcp3.h | 5 +- firmware/export/wm8751.h | 8 ++ 3 files changed, 76 insertions(+), 39 deletions(-) diff --git a/firmware/drivers/audio/wm8751.c b/firmware/drivers/audio/wm8751.c index ecfa751cb2..2f6da1f4e0 100644 --- a/firmware/drivers/audio/wm8751.c +++ b/firmware/drivers/audio/wm8751.c @@ -427,43 +427,6 @@ void audiohw_set_depth_3d(int val) } #endif -#if defined(HAVE_WM8750) && defined(HAVE_FMRADIO_IN) && !defined(HAVE_RECORDING) -/* The FM tuner, on LINPUT1/RINPUT1, played through the analog bypass into - * the output mixers - for a target that plays the radio but cannot record - * it. As on the YP-CP3's original firmware, the signal goes through the - * input PGA at +12 dB, which the tuner's low line level needs. The DAC - * stays in the mix. */ -void audiohw_set_monitor(bool enable) -{ - if (enable) - { - wmcodec_set_masked(ADCL, ADCL_LINSEL_LINPUT1, ADCL_LINSEL_MASK); - wmcodec_set_masked(ADCR, ADCR_RINSEL_RINPUT1, ADCR_RINSEL_MASK); - wmcodec_set_reg(LINVOL, LINVOL_LIZC | LINVOL_LINVOL(0x27)); - wmcodec_set_reg(RINVOL, RINVOL_RIVU | RINVOL_RIZC | - RINVOL_RINVOL(0x27)); - wmcodec_set_bits(PWRMGMT1, PWRMGMT1_AINL | PWRMGMT1_AINR); - - wmcodec_set_masked(LEFTMIX1, LEFTMIX1_LMIXSEL_ADCLIN | - LEFTMIX1_LI2LOVOL(0x20), - 0x7 | LEFTMIX1_LI2LOVOL_MASK); - wmcodec_set_bits(LEFTMIX1, LEFTMIX1_LI2LO); - wmcodec_set_masked(RIGHTMIX1, RIGHTMIX1_RMIXSEL_ADCRIN, 0x7); - wmcodec_set_masked(RIGHTMIX2, RIGHTMIX2_RI2ROVOL(0x20), - RIGHTMIX2_RI2ROVOL_MASK); - wmcodec_set_bits(RIGHTMIX2, RIGHTMIX2_RI2RO); - } - else - { - wmcodec_clear_bits(LEFTMIX1, LEFTMIX1_LI2LO); - wmcodec_clear_bits(RIGHTMIX2, RIGHTMIX2_RI2RO); - wmcodec_set_bits(LINVOL, LINVOL_LINMUTE); - wmcodec_set_bits(RINVOL, RINVOL_RIVU | RINVOL_RINMUTE); - wmcodec_clear_bits(PWRMGMT1, PWRMGMT1_AINL | PWRMGMT1_AINR); - } -} -#endif - #ifdef HAVE_RECORDING #if 0 static void audiohw_set_ngat(int ngath, int type, bool enable) @@ -502,6 +465,70 @@ static void audiohw_set_alc(unsigned char level, /* signal level at ADC */ } #endif +#if defined(SAMSUNG_YPCP3) +/* Samsung YP-CP3: the FM tuner on LINPUT1/RINPUT1, the microphone - mono - + * on RINPUT2, the headphones on OUT2. The rk27xx cannot send received + * samples straight back out, so what is heard of an input goes through the + * codec's analog bypass, the input PGA into the output mixers: the radio + * always, the microphone never. As in the original firmware, the radio + * passes the PGA at +12 dB when only listened to, and the microphone gets + * +13 dB of boost and is recorded by the right ADC onto both channels. + * While recording, the gain setting drives the PGA. */ +void audiohw_set_recsrc(int source, bool recording) +{ + audiohw_mute(true); + + /* inputs off */ + wmcodec_clear_bits(LEFTMIX1, LEFTMIX1_LI2LO); + wmcodec_clear_bits(RIGHTMIX2, RIGHTMIX2_RI2RO); + wmcodec_clear_bits(PWRMGMT1, PWRMGMT1_AINL | PWRMGMT1_AINR | + PWRMGMT1_ADCL | PWRMGMT1_ADCR); + wmcodec_set_masked(ADCR, ADCR_RMICBOOST_DISABLED, 0x3 << 4); + wmcodec_set_masked(ADDITIONAL1, ADDITIONAL1_DATSEL_LR, + ADDITIONAL1_DATSEL_MASK); + + switch (source) + { + case AUDIO_SRC_FMRADIO: + wmcodec_set_masked(ADCL, ADCL_LINSEL_LINPUT1, ADCL_LINSEL_MASK); + wmcodec_set_masked(ADCR, ADCR_RINSEL_RINPUT1, ADCR_RINSEL_MASK); + wmcodec_set_bits(PWRMGMT1, PWRMGMT1_AINL | PWRMGMT1_AINR); + if (recording) + { + wmcodec_set_bits(PWRMGMT1, PWRMGMT1_ADCL | PWRMGMT1_ADCR); + } + else + { + wmcodec_set_reg(LINVOL, LINVOL_LIZC | LINVOL_LINVOL(0x27)); + wmcodec_set_reg(RINVOL, RINVOL_RIVU | RINVOL_RIZC | + RINVOL_RINVOL(0x27)); + } + + wmcodec_set_masked(LEFTMIX1, LEFTMIX1_LMIXSEL_ADCLIN | + LEFTMIX1_LI2LOVOL(0x20), + 0x7 | LEFTMIX1_LI2LOVOL_MASK); + wmcodec_set_bits(LEFTMIX1, LEFTMIX1_LI2LO); + wmcodec_set_masked(RIGHTMIX1, RIGHTMIX1_RMIXSEL_ADCRIN, 0x7); + wmcodec_set_masked(RIGHTMIX2, RIGHTMIX2_RI2ROVOL(0x20), + RIGHTMIX2_RI2ROVOL_MASK); + wmcodec_set_bits(RIGHTMIX2, RIGHTMIX2_RI2RO); + break; + + case AUDIO_SRC_MIC: + wmcodec_set_masked(ADCR, ADCR_RINSEL_RINPUT2 | ADCR_RMICBOOST_13DB, + ADCR_RINSEL_MASK | (0x3 << 4)); + wmcodec_set_bits(PWRMGMT1, PWRMGMT1_AINR | PWRMGMT1_ADCR); + wmcodec_set_masked(ADDITIONAL1, ADDITIONAL1_DATSEL_RR, + ADDITIONAL1_DATSEL_MASK); + break; + + default: /* playback */ + break; + } + + audiohw_mute(false); +} +#else /* !SAMSUNG_YPCP3 */ void audiohw_set_recsrc(int source, bool recording) { /* INPUT1 - FM radio @@ -674,6 +701,7 @@ void audiohw_set_recsrc(int source, bool recording) #endif } /* switch(source) */ } +#endif /* SAMSUNG_YPCP3 */ static int digital_gain2hw(int value) { diff --git a/firmware/export/config/samsungypcp3.h b/firmware/export/config/samsungypcp3.h index 8597510f75..61b80f5477 100644 --- a/firmware/export/config/samsungypcp3.h +++ b/firmware/export/config/samsungypcp3.h @@ -8,7 +8,7 @@ #define MODEL_NAME "Samsung YP-CP3" /* define this if you have recording possibility */ -/* #define HAVE_RECORDING */ +#define HAVE_RECORDING /* define the bitmask of hardware sample rates */ #define HW_SAMPR_CAPS (SAMPR_CAP_44 | SAMPR_CAP_22 | SAMPR_CAP_11 \ @@ -115,7 +115,8 @@ #define HAVE_RDS_CAP #define CONFIG_RDS (RDS_CFG_POLL | RDS_CFG_PROCESS) #define CONFIG_RDS_POLL_TICKS 4 -#define INPUT_SRC_CAPS SRC_CAP_FMRADIO +/* inputs: the microphone (mono, on RINPUT2) and the tuner */ +#define INPUT_SRC_CAPS (SRC_CAP_MIC | SRC_CAP_FMRADIO) /* Define this for LCD backlight available */ #define HAVE_BACKLIGHT diff --git a/firmware/export/wm8751.h b/firmware/export/wm8751.h index bae37a7a92..605a7437ea 100644 --- a/firmware/export/wm8751.h +++ b/firmware/export/wm8751.h @@ -183,6 +183,14 @@ AUDIOHW_SETTING(TREBLE_CUTOFF, "kHz", 0, 4, 4, 8, 4) #define ADDITIONAL1 0x17 #define ADDITIONAL1_TOEN (1 << 0) #define ADDITIONAL1_DACINV (1 << 1) +#if defined(HAVE_WM8750) +/* which ADC feeds the left and right channels of the output data */ +#define ADDITIONAL1_DATSEL_LR (0 << 2) +#define ADDITIONAL1_DATSEL_LL (1 << 2) +#define ADDITIONAL1_DATSEL_RR (2 << 2) +#define ADDITIONAL1_DATSEL_RL (3 << 2) +#define ADDITIONAL1_DATSEL_MASK (3 << 2) +#endif #define ADDITIONAL1_DMONOMIX_LLRR (0 << 4) #define ADDITIONAL1_DMONOMIX_ML0R (1 << 4) #define ADDITIONAL1_DMONOMIX_0LMR (2 << 4) From e1a18fa15c1812d0fb354c0d1a78c8f24cd20d84 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 29 Sep 2026 08:26:49 -0400 Subject: [PATCH 268/408] builds: Relegate Samsung YP-CP3 to "unusable" status for now It's not part of the build farm, doesn't have a manual, and isn't even listed on the www site or wiki yet. Change-Id: I5fc79d0316717898f16ff794cba114a46472ae8f --- tools/builds.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/builds.pm b/tools/builds.pm index eedac927ed..d2e17fa154 100644 --- a/tools/builds.pm +++ b/tools/builds.pm @@ -237,7 +237,7 @@ $releasenotes="/wiki/ReleaseNotes400"; }, 'samsungypcp3' => { name => 'Samsung YP-CP3', - status => 3, + status => 1, }, 'samsungyh820' => { name => 'Samsung YH-820', From 0b2caf73387de04ad1ae70a21ce851b60292c9bb Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 29 Sep 2026 08:44:13 -0400 Subject: [PATCH 269/408] configure: Clean up 'appextra' definitions. The contents of appextra are added to the INCLUDES list, eg: appextra="recorder:hosted" will result in this at compile time: INCLUDES += -I$(APPDIR)/recorder -I$(APPDIR)/hosted With that in mind, * 'gui' and 'recorder' have been set for all targets other than the original charcell Archos. Globally set these instead of using them everywhere. * 'hosted' was effectively a no-op and removed entirely. * 'radio' was specified on a lot of targets that didn't have a radio, just make it global as well. The net result is that only the android targets now define 'appextra' in their configure entry. A future patch will remove the need to specify the ones that are now global. Change-Id: I751284cde4785077c54405a8a10be819021e4255 --- tools/configure | 231 ++++++++++++++++++++++++------------------------ 1 file changed, 115 insertions(+), 116 deletions(-) diff --git a/tools/configure b/tools/configure index cb55218684..4c3d1ff20b 100755 --- a/tools/configure +++ b/tools/configure @@ -1716,23 +1716,23 @@ cat < Date: Tue, 29 Sep 2026 10:16:26 -0400 Subject: [PATCH 270/408] h1x0/h3x0: Get rid of redundant cut-down bidi_l2v() function The one in the main bidi.c is already cut-down for bootloader builds Change-Id: I4465419393b0eda91fb69bd4c3353f7c31e1e98d --- bootloader/iriver_h1x0.c | 14 -------------- bootloader/iriver_h300.c | 14 -------------- 2 files changed, 28 deletions(-) diff --git a/bootloader/iriver_h1x0.c b/bootloader/iriver_h1x0.c index 380d0e91bd..8ac0ab4d06 100644 --- a/bootloader/iriver_h1x0.c +++ b/bootloader/iriver_h1x0.c @@ -599,17 +599,3 @@ int usb_screen(void) { return 0; } - -ucschar_t *bidi_l2v(const unsigned char *str, int orientation) -{ - static ucschar_t utf_buf[SCROLL_LINE_SIZE]; - ucschar_t *target; - (void)orientation; - - target = utf_buf; - - while (*str) - str = utf8decode(str, target++); - *target = 0; - return utf_buf; -} diff --git a/bootloader/iriver_h300.c b/bootloader/iriver_h300.c index 8fb273edb2..5780802ab8 100644 --- a/bootloader/iriver_h300.c +++ b/bootloader/iriver_h300.c @@ -668,17 +668,3 @@ int usb_screen(void) { return 0; } - -ucschar_t *bidi_l2v(const unsigned char *str, int orientation) -{ - static ucschar_t utf_buf[SCROLL_LINE_SIZE]; - ucschar_t *target; - (void)orientation; - - target = utf_buf; - - while (*str) - str = utf8decode(str, target++); - *target = 0; - return utf_buf; -} From bbe37187d8ef9a86f0314529d15ee9ea0bf13910 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 29 Sep 2026 15:12:55 -0400 Subject: [PATCH 271/408] imx233: Increase bootloader's firmware buffer size to 2MB Bootloaders with this change are now able to boot rockbox binaries over 1MB. (1.5MB buffer led to bootloader hanging) Change-Id: I540bd4146aaa7236df93e74f6f2c69aa32e4a874 --- firmware/target/arm/imx233/boot.lds | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/target/arm/imx233/boot.lds b/firmware/target/arm/imx233/boot.lds index 826bed392e..e21711785c 100644 --- a/firmware/target/arm/imx233/boot.lds +++ b/firmware/target/arm/imx233/boot.lds @@ -7,7 +7,7 @@ OUTPUT_ARCH(arm) STARTUP(target/arm/imx233/crt0.o) /* Leave a hole at the beginning of the RAM to load the firmware */ -#define RAM_HOLE 1536 * 1024 +#define RAM_HOLE 2048 * 1024 /* Make a difference between virtual and physical address so that we can use * the resulting elf file with the elftosb tools which loads at the *physical* From 9ab4bdcf91420ead98650c9141741b38a1e89c07 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 29 Sep 2026 18:30:32 -0400 Subject: [PATCH 272/408] Translation updates: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Polish (Adam Rak) Simplified Chinese (Wang Ji) Slovak (Matej Golian) Turkish (Mustafa Yıldız) US English (Myself) Change-Id: Icab7a33af666636ca7c4e7aadd517fbc82b062b2 --- apps/lang/chinese-simp.lang | 12 +- apps/lang/english-us.lang | 12 +- apps/lang/polski.lang | 12 +- apps/lang/slovak.lang | 12 +- apps/lang/turkce.lang | 226 +++++++++++++++++++++++++++++++----- 5 files changed, 224 insertions(+), 50 deletions(-) diff --git a/apps/lang/chinese-simp.lang b/apps/lang/chinese-simp.lang index 3c8be16c72..19273d4842 100644 --- a/apps/lang/chinese-simp.lang +++ b/apps/lang/chinese-simp.lang @@ -8888,15 +8888,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "低频截止频率" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "低频截止频率" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "低频截止频率" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "低频截止频率" @@ -9094,15 +9094,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "高频截止频率" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "高频截止频率" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "高频截止频率" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "高频截止频率" diff --git a/apps/lang/english-us.lang b/apps/lang/english-us.lang index 55028062cd..83cace36fd 100644 --- a/apps/lang/english-us.lang +++ b/apps/lang/english-us.lang @@ -9113,15 +9113,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" @@ -9130,15 +9130,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" diff --git a/apps/lang/polski.lang b/apps/lang/polski.lang index 19320cbe5c..8751ffa555 100644 --- a/apps/lang/polski.lang +++ b/apps/lang/polski.lang @@ -9095,15 +9095,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Ścięcie basów" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Granica basu" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Ścięcie basów" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Granica basu" @@ -9112,15 +9112,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Ścięcie tonów niskich" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Granica tonów wysokich" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Ścięcie tonów niskich" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Granica tonów wysokich" diff --git a/apps/lang/slovak.lang b/apps/lang/slovak.lang index 7f459d8ba5..d44cd4ee08 100644 --- a/apps/lang/slovak.lang +++ b/apps/lang/slovak.lang @@ -9086,15 +9086,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Hranica hĺbok" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Hranica hĺbok" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Hranica hĺbok" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Hranica hĺbok" @@ -9103,15 +9103,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Hranica výšok" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Hranica výšok" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Hranica výšok" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Hranica výšok" diff --git a/apps/lang/turkce.lang b/apps/lang/turkce.lang index 6daad2e8cb..374a89ea82 100644 --- a/apps/lang/turkce.lang +++ b/apps/lang/turkce.lang @@ -5426,15 +5426,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Tiz Kesme" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Tiz Kesme" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Tiz Kesme" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Tiz Kesme" @@ -8629,10 +8629,10 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Tepe Filtresi %d" + *: "Tepe Filtresi" *: "Tepe Filtresi" @@ -9502,15 +9502,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bas Kesme" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bas Kesme" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bas Kesme" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bas Kesme" @@ -14941,20 +14941,6 @@ *: "Eklenti değil" - - id: LANG_OPEN_PLUGIN_SET_WPS_CONTEXT_PLUGIN - desc: open plugin module - user: core - - *: "Set WPS Context Plugin" - - - *: "WPS Bağlam Eklentisini Ayarla" - - - *: "WPS Bağlam Eklentisini Ayarla" - - id: LANG_PARAMETER desc: @@ -15804,16 +15790,16 @@ id: LANG_DEFAULT_BROWSER - desc: in Settings + desc: deprecated user: core - *: "Default Browser" + *: "" - *: "Varsayılan Tarayıcı" + *: "" - *: "Varsayılan Tarayıcı" + *: "" @@ -16988,3 +16974,191 @@ *: "~U S B" + + id: LANG_SET_CONTEXT_ITEM + desc: WPS context item + user: core + + *: "Set Context Item" + + + *: "Öğeyi Ayarla" + + + *: "Öğeyi Ayarla" + + + + id: LANG_FILE_NOT_FOUND + desc: When file does not exist + user: core + + *: "File not found" + + + *: "Dosya bulunamadı" + + + *: "Dosya bulunamadı" + + + + id: LANG_SHOW_IN_FILES + desc: Reveal item in File Browser + user: core + + *: "Show in Files" + + + *: "Dosyalarda Göster" + + + *: "Dosyalarda Göster" + + + + id: LANG_CHANNEL_SWAP + desc: in sound_settings + user: core + + *: "Swap Left & Right" + + + *: "Sol & Sağı Değiştir" + + + *: "Sol & Sağı Değiştir" + + + + id: LANG_COUNTDOWN_TIMER_SET + desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration + user: core + + *: "SET TIMER" + + + *: "ZAMANLAYICI AYARLA" + + + *: "Zamanlayıcı ayarla" + + + + id: LANG_COUNTDOWN_TIMER_RUNNING + desc: countdown_timer plugin - status label shown while the countdown is active + user: core + + *: "RUNNING" + + + *: "ÇALIŞIYOR" + + + *: "Çalışıyor" + + + + id: LANG_COUNTDOWN_TIMER_PAUSED + desc: countdown_timer plugin - status label shown while the countdown is paused + user: core + + *: "PAUSED" + + + *: "DURDURULDU" + + + *: "Durduruldu" + + + + id: LANG_COUNTDOWN_TIMER_OVERTIME + desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up + user: core + + *: "OVERTIME" + + + *: "SÜRE AŞIMI" + + + *: "Süre Aşımı" + + + + id: LANG_COUNTDOWN_TIMER_FINISHED + desc: countdown_timer plugin - status label shown at the moment the countdown expires + user: core + + *: "FINISHED" + + + *: "BİTTİ" + + + *: "Bitti" + + + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Filtre %d" + + + *: "Filtre" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Filtre Tipi" + + + *: "Filtre Tipi" + + + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "Kompozit Video Çıkışı" + + + *: none + videoout: "Kompozit Video Çıkışı" + + + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "Ekran Kaydırma Kısayolları" + + + *: none + touchscreen: "Ekran Kaydırma Kısayolları" + + From f012c09eed755477891b5f85138bca83c5b6671a Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Wed, 30 Sep 2026 00:46:58 +0300 Subject: [PATCH 273/408] Bulgarian translation update Change-Id: Id13ca6875a08eb2e1843dc6ecaa15fd0f09b96c4 --- apps/lang/bulgarian.lang | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/lang/bulgarian.lang b/apps/lang/bulgarian.lang index 5f711a6eb9..ccb7fafdd8 100644 --- a/apps/lang/bulgarian.lang +++ b/apps/lang/bulgarian.lang @@ -9104,15 +9104,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Отрязване на баса" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Отрязване на баса" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Отрязване на баса" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Отрязване на баса" @@ -9121,15 +9121,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Отрязване на високите" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Отрязване на високите" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Отрязване на високите" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Отрязване на високите" From 67ba9c3121f561768486ffa361e4feaece9ac8a5 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 29 Sep 2026 15:15:15 -0400 Subject: [PATCH 274/408] imx233: re-enable utf8proc and associated functionality. Will require updated bootloaders! Change-Id: Id9a3da0b05fe27f7a3d27443a2f15742455d86c2 --- tools/configure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/configure b/tools/configure index 4c3d1ff20b..88ddda707f 100755 --- a/tools/configure +++ b/tools/configure @@ -4777,7 +4777,7 @@ fi # We generally want utf8proc. Exceptions are: # Bootloaders, checkwps, warble, <4MB RAM # and (temporarily) imx233 -if [ "$apps" = "bootloader" -o "$app_type" = "checkwps" -o "$app_type" = "warble" -o "$memory" -lt "4" -o "$t_manufacturer" = "imx233" ]; then +if [ "$apps" = "bootloader" -o "$app_type" = "checkwps" -o "$app_type" = "warble" -o "$memory" -lt "4" ]; then no_utf8proc=1 fi From b6b2d953095e1fa2fce71c1f75c15aadaaed720a Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Wed, 23 Sep 2026 21:43:24 -0400 Subject: [PATCH 275/408] opus: fixed-phase SILK resampler interpolation The 8, 12 and 16 kHz to 48 kHz steps visit only two or three FIR phases in a fixed cycle, so each set of input samples is read once and reused across outputs. Bit-exact; OPUS_NO_SILK_FIXED_PHASE disables it. Measured with silk_5.opus (WB SILK): e200v1: 36.31 -> 33.38 MHz, -8.1% Clip+: 21.66 -> 20.93 MHz, -3.4% Co-Authored-By: Claude Opus 5.5 Change-Id: Icb0f8ded62739dfee1f574e4888a54c778aa3d53 --- lib/rbcodec/codecs/libopus/README.rockbox | 4 + .../libopus/silk/resampler_private_IIR_FIR.c | 88 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/lib/rbcodec/codecs/libopus/README.rockbox b/lib/rbcodec/codecs/libopus/README.rockbox index f32d2d007d..41d38363f2 100644 --- a/lib/rbcodec/codecs/libopus/README.rockbox +++ b/lib/rbcodec/codecs/libopus/README.rockbox @@ -18,6 +18,10 @@ Celt: silk, since config.h defines exactly one of them per core * add #define ABS(a)(((a) < 0) ? - (a) :(a)) to mathops.h +Silk: +* added fixed-phase interpolation paths for the 8, 12 and 16 kHz to 48 kHz + steps in resampler_private_IIR_FIR.c (disable with OPUS_NO_SILK_FIXED_PHASE) + Opus-tools: * copied src/opus_header.h and src/opus_header.c to lib/rbcodec/codecs/libopus * changed #include to #include "ogg/ogg.h" in opus_header.h diff --git a/lib/rbcodec/codecs/libopus/silk/resampler_private_IIR_FIR.c b/lib/rbcodec/codecs/libopus/silk/resampler_private_IIR_FIR.c index 6b2b3a2e18..c09551d3a0 100644 --- a/lib/rbcodec/codecs/libopus/silk/resampler_private_IIR_FIR.c +++ b/lib/rbcodec/codecs/libopus/silk/resampler_private_IIR_FIR.c @@ -33,6 +33,83 @@ POSSIBILITY OF SUCH DAMAGE. #include "resampler_private.h" #include "stack_alloc.h" +#ifndef OPUS_NO_SILK_FIXED_PHASE +/* One output at phase p from eight consecutive samples: the same sum as the + generic loop below, but as plain 16x16 C multiplies the compiler can + schedule, where silk_SMLABB is inline asm on ARMv5E. */ +static OPUS_INLINE opus_int16 silk_IIR_FIR_tap8( + opus_int16 x0, opus_int16 x1, opus_int16 x2, opus_int16 x3, + opus_int16 x4, opus_int16 x5, opus_int16 x6, opus_int16 x7, + opus_int p +) +{ + const opus_int16 *a = silk_resampler_frac_FIR_12[ p ]; + const opus_int16 *b = silk_resampler_frac_FIR_12[ 11 - p ]; + opus_int32 res_Q15; + + res_Q15 = (opus_int32)x0 * a[ 0 ]; + res_Q15 += (opus_int32)x1 * a[ 1 ]; + res_Q15 += (opus_int32)x2 * a[ 2 ]; + res_Q15 += (opus_int32)x3 * a[ 3 ]; + res_Q15 += (opus_int32)x4 * b[ 3 ]; + res_Q15 += (opus_int32)x5 * b[ 2 ]; + res_Q15 += (opus_int32)x6 * b[ 1 ]; + res_Q15 += (opus_int32)x7 * b[ 0 ]; + return (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( res_Q15, 15 ) ); +} + +/* The steps silk_resampler_init produces for 16 and 8 kHz to 48 kHz visit + only three phases, in a fixed cycle, for any batch up to + RESAMPLER_MAX_BATCH_SIZE_IN: the rounding in the step never drifts across + a phase boundary before the index restarts. Checked exhaustively over + every batch length. The third output's window starts off2 samples later, + and each cycle advances adv samples. */ +static OPUS_INLINE opus_int16 *silk_IIR_FIR_cycle3( + opus_int16 *out, const opus_int16 *buf, opus_int32 max_index_Q16, + opus_int32 inc, opus_int p0, opus_int p1, opus_int p2, opus_int off2, + opus_int adv +) +{ + opus_int32 index_Q16 = 0; + + while( index_Q16 < max_index_Q16 ) { + opus_int16 x0 = buf[ 0 ], x1 = buf[ 1 ], x2 = buf[ 2 ], x3 = buf[ 3 ]; + opus_int16 x4 = buf[ 4 ], x5 = buf[ 5 ], x6 = buf[ 6 ], x7 = buf[ 7 ]; + *out++ = silk_IIR_FIR_tap8( x0, x1, x2, x3, x4, x5, x6, x7, p0 ); + if( ( index_Q16 += inc ) >= max_index_Q16 ) break; + *out++ = silk_IIR_FIR_tap8( x0, x1, x2, x3, x4, x5, x6, x7, p1 ); + if( ( index_Q16 += inc ) >= max_index_Q16 ) break; + if( off2 ) { + *out++ = silk_IIR_FIR_tap8( x1, x2, x3, x4, x5, x6, x7, buf[ 8 ], p2 ); + } else { + *out++ = silk_IIR_FIR_tap8( x0, x1, x2, x3, x4, x5, x6, x7, p2 ); + } + index_Q16 += inc; + buf += adv; + } + return out; +} + +/* 12 kHz to 48 kHz steps by exactly half a sample: phases 0 and 6. */ +static OPUS_INLINE opus_int16 *silk_IIR_FIR_cycle2( + opus_int16 *out, const opus_int16 *buf, opus_int32 max_index_Q16 +) +{ + opus_int32 index_Q16 = 0; + + while( index_Q16 < max_index_Q16 ) { + opus_int16 x0 = buf[ 0 ], x1 = buf[ 1 ], x2 = buf[ 2 ], x3 = buf[ 3 ]; + opus_int16 x4 = buf[ 4 ], x5 = buf[ 5 ], x6 = buf[ 6 ], x7 = buf[ 7 ]; + *out++ = silk_IIR_FIR_tap8( x0, x1, x2, x3, x4, x5, x6, x7, 0 ); + if( ( index_Q16 += 32768 ) >= max_index_Q16 ) break; + *out++ = silk_IIR_FIR_tap8( x0, x1, x2, x3, x4, x5, x6, x7, 6 ); + index_Q16 += 32768; + buf += 1; + } + return out; +} +#endif + static OPUS_INLINE opus_int16 *silk_resampler_private_IIR_FIR_INTERPOL( opus_int16 *out, opus_int16 *buf, @@ -44,6 +121,17 @@ static OPUS_INLINE opus_int16 *silk_resampler_private_IIR_FIR_INTERPOL( opus_int16 *buf_ptr; opus_int32 table_index; +#ifndef OPUS_NO_SILK_FIXED_PHASE + switch( index_increment_Q16 ) { + case 43691: /* 16 kHz */ + return silk_IIR_FIR_cycle3( out, buf, max_index_Q16, 43691, 0, 8, 4, 1, 2 ); + case 21846: /* 8 kHz */ + return silk_IIR_FIR_cycle3( out, buf, max_index_Q16, 21846, 0, 4, 8, 0, 1 ); + case 32768: /* 12 kHz */ + return silk_IIR_FIR_cycle2( out, buf, max_index_Q16 ); + } +#endif + /* Interpolate upsampled signal and store in output array */ for( index_Q16 = 0; index_Q16 < max_index_Q16; index_Q16 += index_increment_Q16 ) { table_index = silk_SMULWB( index_Q16 & 0xFFFF, 12 ); From 88f3d21890dae029efaf28ce92d66f4d9495e20a Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Tue, 29 Sep 2026 23:01:02 +0300 Subject: [PATCH 276/408] manual: Fix case of "iPod" The special iPod_Control folder's case is also fixed. Change-Id: I9a65cf9034774ddb8c3b2de00ff1c27e35710d20 --- manual/getting_started/installation.tex | 2 +- manual/platform/ipod1g2g.tex | 2 +- manual/platform/ipod3g.tex | 2 +- manual/platform/ipod4g.tex | 2 +- manual/platform/ipod6g.tex | 2 +- manual/platform/ipodcolor.tex | 2 +- manual/platform/ipodmini1g.tex | 2 +- manual/platform/ipodnano1g.tex | 2 +- manual/platform/ipodnano2g.tex | 2 +- manual/platform/ipodnano3g.tex | 2 +- manual/platform/ipodvideo.tex | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/manual/getting_started/installation.tex b/manual/getting_started/installation.tex index 8a78b84fbd..dbea5b335f 100644 --- a/manual/getting_started/installation.tex +++ b/manual/getting_started/installation.tex @@ -852,7 +852,7 @@ you turn the unit on, Rockbox should load.} If you have loaded music onto your \dap{} using iTunes, you will not be able to see your music properly in the \setting{File Browser}. This is because iTunes changes your files' names and hides them in - directories in the \fname{Ipod\_Control} directory. Files placed on your + directories in the \fname{iPod\_Control} directory. Files placed on your \dap{} using iTunes can be viewed by initialising and using Rockbox's database. See \reference{ref:database} for more information. } diff --git a/manual/platform/ipod1g2g.tex b/manual/platform/ipod1g2g.tex index eeed6c4e3a..df6ca63e40 100644 --- a/manual/platform/ipod1g2g.tex +++ b/manual/platform/ipod1g2g.tex @@ -5,7 +5,7 @@ \edef\UseOption{\UseOption,HAVE_DISK_STORAGE} \edef\UseOption{\UseOption,ipod} -\newcommand{\playerman}{Ipod} +\newcommand{\playerman}{iPod} \newcommand{\playertype}{1G / 2G} \newcommand{\playerlongtype}{\playertype} \newcommand{\dapdisplaywidth}{160} diff --git a/manual/platform/ipod3g.tex b/manual/platform/ipod3g.tex index bd4fffd21b..ff694a0a95 100644 --- a/manual/platform/ipod3g.tex +++ b/manual/platform/ipod3g.tex @@ -5,7 +5,7 @@ \edef\UseOption{\UseOption,HAVE_DISK_STORAGE} \edef\UseOption{\UseOption,ipod} -\newcommand{\playerman}{Ipod} +\newcommand{\playerman}{iPod} \newcommand{\playertype}{3G} \newcommand{\playerlongtype}{\playertype} \newcommand{\dapdisplaywidth}{160} diff --git a/manual/platform/ipod4g.tex b/manual/platform/ipod4g.tex index 501a508109..226ae870bc 100644 --- a/manual/platform/ipod4g.tex +++ b/manual/platform/ipod4g.tex @@ -5,7 +5,7 @@ \edef\UseOption{\UseOption,HAVE_DISK_STORAGE} \edef\UseOption{\UseOption,ipod} -\newcommand{\playerman}{Ipod} +\newcommand{\playerman}{iPod} \newcommand{\playertype}{4G} \newcommand{\playerlongtype}{\playertype} \newcommand{\dapdisplaywidth}{160} diff --git a/manual/platform/ipod6g.tex b/manual/platform/ipod6g.tex index 078d7a75c1..3feb381c65 100644 --- a/manual/platform/ipod6g.tex +++ b/manual/platform/ipod6g.tex @@ -8,7 +8,7 @@ \edef\UseOption{\UseOption,HAVE_HW_H264} \edef\UseOption{\UseOption,ipod} -\newcommand{\playerman}{Ipod} +\newcommand{\playerman}{iPod} \newcommand{\playertype}{Classic} \newcommand{\playerlongtype}{\playertype} \newcommand{\dapdisplaywidth}{320} diff --git a/manual/platform/ipodcolor.tex b/manual/platform/ipodcolor.tex index 1264ad4298..517d4fb82d 100644 --- a/manual/platform/ipodcolor.tex +++ b/manual/platform/ipodcolor.tex @@ -5,7 +5,7 @@ \edef\UseOption{\UseOption,HAVE_DISK_STORAGE} \edef\UseOption{\UseOption,ipod} -\newcommand{\playerman}{Ipod} +\newcommand{\playerman}{iPod} \newcommand{\playertype}{Color} \newcommand{\playerlongtype}{\playertype} \newcommand{\dapdisplaywidth}{220} diff --git a/manual/platform/ipodmini1g.tex b/manual/platform/ipodmini1g.tex index c884ab16de..ec5a580360 100644 --- a/manual/platform/ipodmini1g.tex +++ b/manual/platform/ipodmini1g.tex @@ -5,7 +5,7 @@ \edef\UseOption{\UseOption,HAVE_DISK_STORAGE} \edef\UseOption{\UseOption,ipod} -\newcommand{\playerman}{Ipod} +\newcommand{\playerman}{iPod} \newcommand{\playertype}{Mini} \newcommand{\playerlongtype}{\playertype} \newcommand{\dapdisplaywidth}{138} diff --git a/manual/platform/ipodnano1g.tex b/manual/platform/ipodnano1g.tex index f1f6ab2735..eb33908325 100644 --- a/manual/platform/ipodnano1g.tex +++ b/manual/platform/ipodnano1g.tex @@ -4,7 +4,7 @@ \edef\UseOption{\UseOption,HAVE_BACKLIGHT} \edef\UseOption{\UseOption,ipod} -\newcommand{\playerman}{Ipod} +\newcommand{\playerman}{iPod} \newcommand{\playertype}{Nano} \newcommand{\playerlongtype}{\playertype{} 1st generation} \newcommand{\dapdisplaywidth}{176} diff --git a/manual/platform/ipodnano2g.tex b/manual/platform/ipodnano2g.tex index a807f5652f..09ad1cafc3 100644 --- a/manual/platform/ipodnano2g.tex +++ b/manual/platform/ipodnano2g.tex @@ -4,7 +4,7 @@ \edef\UseOption{\UseOption,HAVE_BACKLIGHT} \edef\UseOption{\UseOption,ipod} -\newcommand{\playerman}{Ipod} +\newcommand{\playerman}{iPod} \newcommand{\playertype}{Nano} \newcommand{\playerlongtype}{\playertype{} 2nd generation} \newcommand{\dapdisplaywidth}{176} diff --git a/manual/platform/ipodnano3g.tex b/manual/platform/ipodnano3g.tex index 8a4b257aab..b273eb5e75 100644 --- a/manual/platform/ipodnano3g.tex +++ b/manual/platform/ipodnano3g.tex @@ -4,7 +4,7 @@ \edef\UseOption{\UseOption,HAVE_BACKLIGHT} \edef\UseOption{\UseOption,ipod} -\newcommand{\playerman}{Ipod} +\newcommand{\playerman}{iPod} \newcommand{\playertype}{Nano} \newcommand{\playerlongtype}{\playertype{} 3rd generation} \newcommand{\dapdisplaywidth}{320} diff --git a/manual/platform/ipodvideo.tex b/manual/platform/ipodvideo.tex index 9699a20c55..5368367925 100644 --- a/manual/platform/ipodvideo.tex +++ b/manual/platform/ipodvideo.tex @@ -6,7 +6,7 @@ \edef\UseOption{\UseOption,HAVE_CAR_ADAPTER_MODE} \edef\UseOption{\UseOption,ipod} -\newcommand{\playerman}{Ipod} +\newcommand{\playerman}{iPod} \newcommand{\playertype}{Video} \newcommand{\playerlongtype}{\playertype} \newcommand{\dapdisplaywidth}{320} From 19caf2756756c9cf35484dc30afb94c594548c42 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 30 Sep 2026 07:57:50 +0200 Subject: [PATCH 277/408] rk27xx codec: mix in only the tuner's line, keep the DAC audiohw_set_monitor() switched the output mixer from the DAC to both line bypasses: voice and beeps went silent while the radio played, and a line nobody listened to was mixed in, with its noise. Both line inputs were also powered from start-up on. - the target names the line its tuner is on, RK27XX_CODEC_FM_LINE (1 or 2); both are used where it does not say - monitoring adds that line's bypass to the DAC instead of replacing it - the line inputs stay in standby until monitored, and go back after Only rk27generic uses the internal codec - the other rk27xx targets have external DACs or codecs. Tested on rk27generic: the radio plays through the monitored line, key clicks stay audible over it, and playback is unaffected. Co-Authored-By: Claude Opus 5.5 Change-Id: I177fdf605e4d997159898c868b2186c9584eb19a --- firmware/drivers/audio/rk27xx_codec.c | 40 +++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/firmware/drivers/audio/rk27xx_codec.c b/firmware/drivers/audio/rk27xx_codec.c index 4357a4c356..3b06d2ba40 100644 --- a/firmware/drivers/audio/rk27xx_codec.c +++ b/firmware/drivers/audio/rk27xx_codec.c @@ -39,8 +39,28 @@ static int codec_read(uint8_t reg, uint8_t *val) } #endif +/* The line input the FM tuner is wired to: RK27XX_CODEC_FM_LINE, 1 or 2, + * from the target config; both when the target does not say. Only the + * tuner's line is bypassed to the output mixer and powered, and only while + * it is being listened to. */ +#if defined(RK27XX_CODEC_FM_LINE) && RK27XX_CODEC_FM_LINE == 1 +#define FM_BYPASS BYPASS1 +#define FM_IN_SB SB_IN1 +#elif defined(RK27XX_CODEC_FM_LINE) && RK27XX_CODEC_FM_LINE == 2 +#define FM_BYPASS BYPASS2 +#define FM_IN_SB SB_IN2 +#else +#define FM_BYPASS (BYPASS1|BYPASS2) +#define FM_IN_SB (SB_IN1|SB_IN2) +#endif + +/* what feeds the output mixer: the DAC, and the tuner's line while the + * radio plays - the DAC stays in the mix, for voice and beeps */ static uint8_t cr1_sel = DACSEL; +/* power: the line inputs in standby unless monitored */ +static uint8_t pmr1 = SB_ADC|SB_MIC|SB_IND|SB_IN1|SB_IN2; + static void audiohw_mute(bool mute) { if (mute) @@ -95,7 +115,7 @@ void audiohw_postinit(void) udelay(1000); /* power up output stage */ - codec_write(PMR1, SB_ADC|SB_MIC|SB_IND); + codec_write(PMR1, pmr1); sleep(HZ/10); GPIO_PDDR |= (1<<7); /* PD7 high */ @@ -152,6 +172,20 @@ void audiohw_set_volume(int vol_l, int vol_r) void audiohw_set_monitor(bool enable) { - cr1_sel = enable ? BYPASS1|BYPASS2 : DACSEL; - codec_write(CR1, cr1_sel|SB_MICBIAS); + if (enable) + { + /* input up before it is mixed in */ + pmr1 &= ~FM_IN_SB; + codec_write(PMR1, pmr1); + cr1_sel = DACSEL|FM_BYPASS; + codec_write(CR1, cr1_sel|SB_MICBIAS); + } + else + { + /* out of the mix before the input goes down */ + cr1_sel = DACSEL; + codec_write(CR1, cr1_sel|SB_MICBIAS); + pmr1 |= FM_IN_SB; + codec_write(PMR1, pmr1); + } } From 89e9ade85560f7bdc5e39713a64cba36a2575957 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 30 Sep 2026 07:57:50 +0200 Subject: [PATCH 278/408] rk27generic: FM radio screen keys, tuner noted as an RDA5807P The FM screen had no actions for the rk27xx generic keypad; give it those its keymap already maps (menu, play, stop, exit) in radio.c. The board's tuner is an RDA5807P. It keeps being driven as a TEA5767, in the chip's compatible mode, as the original firmware does: tuning, seek and the stereo indicator work so, and the RDA mode would bring nothing here - this variant has no RDS. Say so next to CONFIG_TUNER. The tuner's audio is on the codec's line input 1: only that line is powered and mixed in while the radio plays (RK27XX_CODEC_FM_LINE 1). With line 2 instead the radio is silent. Tested on the rk27generic board: manual tuning, seek, stereo indicator and audio. Co-Authored-By: Claude Opus 5.5 Change-Id: I2135ee484705ff0fce6d88e73288d8ed2aec6c2d --- apps/radio/radio.c | 6 ++++++ firmware/export/config/rk27generic.h | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/apps/radio/radio.c b/apps/radio/radio.c index 03639258c1..9b68c948c2 100644 --- a/apps/radio/radio.c +++ b/apps/radio/radio.c @@ -97,6 +97,12 @@ #define FM_PLAY #define FM_MODE +#elif (CONFIG_KEYPAD == RK27XX_GENERIC_PAD) +#define FM_MENU +#define FM_PLAY +#define FM_STOP +#define FM_EXIT + #elif (CONFIG_KEYPAD == SAMSUNG_YPR0_PAD) #define FM_MENU #define FM_PRESET diff --git a/firmware/export/config/rk27generic.h b/firmware/export/config/rk27generic.h index 6ad98756ed..c31efd0a7e 100644 --- a/firmware/export/config/rk27generic.h +++ b/firmware/export/config/rk27generic.h @@ -109,8 +109,12 @@ /* Define the type of audio codec */ #define HAVE_RK27XX_CODEC +/* The tuner is an RDA5807P, driven in its TEA5767 compatible mode as the + * original firmware does - it has no RDS, and its own mode brings nothing */ #define CONFIG_TUNER TEA5767 #define CONFIG_TUNER_XTAL 32768 +/* its audio is on the codec's line input 1 */ +#define RK27XX_CODEC_FM_LINE 1 /* #define HAVE_PCM_DMA_ADDRESS */ From 15eec82d8f9fb7c782a4371664bdadd474cb98a5 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 28 Sep 2026 10:14:58 -0400 Subject: [PATCH 279/408] opus: ARMv4 SILK resampler interpolation On ARMv4 the fixed-phase FIR cycles at 8, 12 and 16 kHz run as adds of shifted samples rather than multiplies: every coefficient is a constant, and a shifted add is one cycle where mla plus loading the coefficient is five or six. Each sample is loaded once per cycle and added into the two or three outputs it feeds, sharing partial products such as 31x between them, about 27 adds per output. The kernels are generated by silk/arm/gen_resampler_armv4.py. Bit-exact; OPUS_ARM_NO_SILK_ASM disables them. Measured on the e200v1, silk_5.opus (WB SILK), with the previous commit: 36.31 -> 28.85 MHz, -20.5%. Co-Authored-By: Claude Opus 5.5 Change-Id: Ic1fd5777e91d182248f43954e4b6ae977309dc5d --- lib/rbcodec/codecs/libopus/README.rockbox | 3 + lib/rbcodec/codecs/libopus/SOURCES | 1 + .../libopus/silk/arm/gen_resampler_armv4.py | 274 ++++++++++++++ .../libopus/silk/arm/resampler_armv4_asm.S | 356 ++++++++++++++++++ .../libopus/silk/resampler_private_IIR_FIR.c | 39 ++ 5 files changed, 673 insertions(+) create mode 100644 lib/rbcodec/codecs/libopus/silk/arm/gen_resampler_armv4.py create mode 100644 lib/rbcodec/codecs/libopus/silk/arm/resampler_armv4_asm.S diff --git a/lib/rbcodec/codecs/libopus/README.rockbox b/lib/rbcodec/codecs/libopus/README.rockbox index 41d38363f2..9c6eefe4a9 100644 --- a/lib/rbcodec/codecs/libopus/README.rockbox +++ b/lib/rbcodec/codecs/libopus/README.rockbox @@ -21,6 +21,9 @@ Celt: Silk: * added fixed-phase interpolation paths for the 8, 12 and 16 kHz to 48 kHz steps in resampler_private_IIR_FIR.c (disable with OPUS_NO_SILK_FIXED_PHASE) +* resampler_private_IIR_FIR.c calls an ARMv4 kernel for those three steps, + silk/arm/resampler_armv4_asm.S, generated by gen_resampler_armv4.py beside + it (disable with OPUS_ARM_NO_SILK_ASM) Opus-tools: * copied src/opus_header.h and src/opus_header.c to lib/rbcodec/codecs/libopus diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index baf2751702..bacf07dd86 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -16,6 +16,7 @@ celt/arm/comb_filter_armv4_asm.S celt/arm/denorm_armv4_asm.S celt/arm/deemph_armv4_asm.S celt/arm/exp_rotation1_armv4_asm.S +silk/arm/resampler_armv4_asm.S #elif defined(CPU_ARM) && (ARM_ARCH >= 5) && (ARCH_PROFILE != ARM_PROFILE_MICRO) celt/arm/kiss_fft_armv5e_asm.S celt/arm/mdct_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/silk/arm/gen_resampler_armv4.py b/lib/rbcodec/codecs/libopus/silk/arm/gen_resampler_armv4.py new file mode 100644 index 0000000000..a39d5d0ed0 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/silk/arm/gen_resampler_armv4.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +"""Generate silk/arm/resampler_armv4_asm.S. + +The FIR half of silk_resampler_private_IIR_FIR at 8, 12 and 16 kHz uses two +or three fixed phase filters in a fixed cycle (see the C), so every +coefficient is a compile-time constant. On ARM7TDMI a constant multiply is +cheaper as shifts and adds than as mla: each add with a shifted operand is +one cycle, where mla is 3-4 cycles plus a load of the coefficient. + +The kernel is sample-major: each input sample is loaded once per cycle and +added into the two or three accumulators it feeds, so a partial product +such as x*(1+2^a) can be shared between the coefficients one sample meets. +The search below finds, per sample, the cheapest set of up to two such +temporaries and the fewest shifted terms per coefficient. + + python3 gen_resampler_armv4.py > resampler_armv4_asm.S +""" +import itertools +import random +import sys +from functools import lru_cache + +T = [[189, -600, 617, 30567], [117, -159, -1070, 29704], + [52, 221, -2392, 28276], [-4, 529, -3350, 26341], + [-48, 758, -3956, 23973], [-80, 905, -4235, 21254], + [-99, 972, -4222, 18278], [-107, 967, -3957, 15143], + [-103, 896, -3487, 11950], [-91, 773, -2865, 8798], + [-71, 611, -2143, 5784], [-46, 425, -1375, 2996]] +# The eight taps of phase p, in sample order, as the C applies them. +P = {p: T[p] + T[11 - p][::-1] for p in range(12)} + +LIM = 1 << 17 + + +def csd(c): + """Canonical signed digits of c as (sign, shift).""" + d, e = [], 0 + while c: + if c & 1: + r = 2 - (c & 3) + d.append((r, e)) + c -= r + c >>= 1 + e += 1 + return d + + +@lru_cache(None) +def singles(V): + s = {} + for v in V: + k = 0 + while (v << k) < LIM: + for sg in (1, -1): + s.setdefault(sg * (v << k), (sg, v, k)) + k += 1 + return s + + +@lru_cache(None) +def doubles(V): + s1 = singles(V) + d = {} + for a, b in itertools.product(s1.items(), repeat=2): + d.setdefault(a[0] + b[0], (a[1], b[1])) + return d + + +@lru_cache(None) +def terms(c, V): + """Fewest (sign, v, shift) terms, v in V, summing to c (None if > 4).""" + s1, s2 = singles(V), doubles(V) + if c in s1: + return [s1[c]] + if c in s2: + return list(s2[c]) + for a, ta in s1.items(): + if c - a in s2: + return [ta] + list(s2[c - a]) + for a, ta in s2.items(): + if c - a in s2: + return list(ta) + list(s2[c - a]) + return None + + +def plain(c): + return [(sg, 1, k) for sg, k in csd(c)] + + +TEMPS = sorted({(1 << a) + 1 for a in range(1, 15)} | + {(1 << a) - 1 for a in range(2, 15)}) + + +def program(cs): + """(temps, [terms per coefficient]) with the fewest instructions.""" + best = (sum(len(csd(c)) for c in cs), (), [plain(c) for c in cs]) + for n in (1, 2): + for ts in itertools.combinations(TEMPS, n): + V = (1,) + ts + tl = [] + cost = n + for c in cs: + t = terms(c, V) + if t is None or len(t) > len(csd(c)): + t = plain(c) + tl.append(t) + cost += len(t) + if cost >= best[0]: + break + else: + best = (cost, ts, tl) + return best + + +# Rates: step, phases in cycle order, window offset of each output, samples +# advanced per cycle. +RATES = [ + ("16k", 43691, [0, 8, 4], [0, 0, 1], 2), + ("8k", 21846, [0, 4, 8], [0, 0, 0], 1), + ("12k", 32768, [0, 6], [0, 0], 1), +] + +ACC = ["r3", "r4", "r5"] +XR = ["r6", "r7"] +TR = ["r8", "r9"] + + +def gen_rate(name, step, phases, offs, adv, out): + nsamp = max(offs) + 8 + # coefficients each sample meets, per accumulator + need = [] + for j in range(nsamp): + cs = [] + for o, (p, off) in enumerate(zip(phases, offs)): + i = j - off + if 0 <= i < 8: + cs.append((o, P[p][i])) + need.append(cs) + # Process x1 .. x(n-1), then x0, whose load post-increments the pointer. + order = list(range(1, nsamp)) + [0] + progs = {} + total = 0 + for j in order: + cost, ts, tl = program(tuple(c for _, c in need[j])) + progs[j] = (ts, tl) + total += cost + # Check every program arithmetically before emitting it. + for j in order: + ts, tl = progs[j] + for x in (1, -1, 32767, -32768, random.randint(-32768, 32767)): + val = {1: x} + for t in ts: + val[t] = x * t + for (o, c), tt in zip(need[j], tl): + assert sum(sg * (val[v] << k) for sg, v, k in tt) == c * x, \ + (name, j, c, tt) + + w = out.write + fn = "silk_IIR_FIR_%s_armv4" % name + w("\n/* %s: phases %s, %d samples a cycle, advancing %d. %d shifted" + " adds a\n cycle, %.1f an output. */\n" + % (name, "/".join(map(str, phases)), nsamp, adv, total, + total / len(phases))) + w(" .global %s\n .type %s, %%function\n%s:\n" % (fn, fn, fn)) + w(" push {r4-r11, lr}\n") + w(" mov r10, #0x7f00\n orr r10, r10, #0xff @ 0x7fff, for the clamp\n") + w(" mov r11, #0x4000 @ the rounding bias\n") + w(" ldrsh %s, [r1, #2] @ x1\n" % XR[0]) + w(".L%s_loop:\n" % name) + started = set() + for n, j in enumerate(order): + xr = XR[n % 2] + ts, tl = progs[j] + body = [] + treg = {1: xr} + for t, r in zip(ts, TR): + treg[t] = r + if (t - 1) & (t - 2) == 0: # 2^a + 1 + a = (t - 1).bit_length() - 1 + body.append("add %s, %s, %s, lsl #%d" % (r, xr, xr, a)) + else: # 2^a - 1 + a = (t + 1).bit_length() - 1 + assert (1 << a) - 1 == t + body.append("rsb %s, %s, %s, lsl #%d" % (r, xr, xr, a)) + for (o, c), tt in zip(need[j], tl): + acc = ACC[o] + first = len(body) + for sg, v, k in tt: + sh = (", lsl #%d" % k) if k else "" + base = acc if o in started else "r11" + started.add(o) + body.append("%s %s, %s, %s%s" % ("add" if sg > 0 else "sub", + acc, base, treg[v], sh)) + body[first] += " " * max(1, 34 - len(body[first])) + \ + "@ x%d * %d" % (j, c) + # The next sample's load goes after this program's first instruction, + # into the register the sample before this one has finished with. + if n + 1 < len(order): + nj = order[n + 1] + if nj == 0: + ld = "ldrsh %s, [r1], #%d" % (XR[(n + 1) % 2], 2 * adv) + ld += " " * max(1, 34 - len(ld)) + "@ x0, and advance" + else: + ld = "ldrsh %s, [r1, #%d]" % (XR[(n + 1) % 2], 2 * nj) + ld += " " * max(1, 34 - len(ld)) + "@ x%d" % nj + body.insert(1, ld) + for b in body: + w(" " + b + "\n") + # Clamp and store; the next cycle's x1 loads under the first clamp. + for o in range(len(phases)): + acc, r = ACC[o], TR[o % 2] + w(" teq %s, %s, lsl #1 @ out of 16 bits?\n" % (acc, acc)) + if o == 0: + w(" ldrsh %s, [r1, #2] @ next x1\n" % XR[0]) + w(" mov %s, %s, asr #15\n" % (r, acc)) + w(" eormi %s, r10, %s, asr #31\n" % (r, acc)) + w(" strh %s, [r0], #2\n" % r) + w(" subs r2, r2, #1\n") + w(" bne .L%s_loop\n" % name) + w(" pop {r4-r11, pc}\n") + w(" .size %s, .-%s\n" % (fn, fn)) + return total + + +HEAD = """/* Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * silk/resampler_private_IIR_FIR.c are met. + */ + +/* GENERATED by silk/arm/gen_resampler_armv4.py -- edit that, not this. + * + * ARMv4 FIR interpolation for silk_resampler_private_IIR_FIR at the three + * SILK rates. Each step visits two or three fixed phase filters in a fixed + * cycle, so every coefficient is a constant, and on ARM7TDMI a constant + * multiply is cheaper as adds of shifted operands (one cycle each) than as + * mla (three or four, plus loading the coefficient). + * + * Sample-major: each input sample is loaded once a cycle and added into the + * two or three accumulators it feeds, sharing a partial product x*(2^a+-1) + * between them where that saves an add. The rounding bias rides in on + * each accumulator's first add. Every sum is exact in 32 bits: the largest + * phase has sum |c| = 50,045, so |sum| < 2^31 for 16-bit input. + * + * opus_int16 *silk_IIR_FIR__armv4(opus_int16 *out, + * const opus_int16 *buf, int cycles) + * + * Runs `cycles` whole cycles (cycles > 0) and returns the advanced out. + * r0 out, r1 buf, r2 cycles, r3-r5 accumulators, r6/r7 samples, r8/r9 + * partial products, r10 0x7fff, r11 the rounding bias. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "resampler_armv4_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 +""" + + +def main(): + random.seed(1) + out = sys.stdout + out.write(HEAD) + tot = {} + for r in RATES: + tot[r[0]] = gen_rate(*r, out=out) + sys.stderr.write("adds per cycle: %s\n" % tot) + + +if __name__ == "__main__": + main() diff --git a/lib/rbcodec/codecs/libopus/silk/arm/resampler_armv4_asm.S b/lib/rbcodec/codecs/libopus/silk/arm/resampler_armv4_asm.S new file mode 100644 index 0000000000..df7983eeb6 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/silk/arm/resampler_armv4_asm.S @@ -0,0 +1,356 @@ +/* Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * silk/resampler_private_IIR_FIR.c are met. + */ + +/* GENERATED by silk/arm/gen_resampler_armv4.py -- edit that, not this. + * + * ARMv4 FIR interpolation for silk_resampler_private_IIR_FIR at the three + * SILK rates. Each step visits two or three fixed phase filters in a fixed + * cycle, so every coefficient is a constant, and on ARM7TDMI a constant + * multiply is cheaper as adds of shifted operands (one cycle each) than as + * mla (three or four, plus loading the coefficient). + * + * Sample-major: each input sample is loaded once a cycle and added into the + * two or three accumulators it feeds, sharing a partial product x*(2^a+-1) + * between them where that saves an add. The rounding bias rides in on + * each accumulator's first add. Every sum is exact in 32 bits: the largest + * phase has sum |c| = 50,045, so |sum| < 2^31 for 16-bit input. + * + * opus_int16 *silk_IIR_FIR__armv4(opus_int16 *out, + * const opus_int16 *buf, int cycles) + * + * Runs `cycles` whole cycles (cycles > 0) and returns the advanced out. + * r0 out, r1 buf, r2 cycles, r3-r5 accumulators, r6/r7 samples, r8/r9 + * partial products, r10 0x7fff, r11 the rounding bias. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "resampler_armv4_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 + +/* 16k: phases 0/8/4, 9 samples a cycle, advancing 2. 82 shifted adds a + cycle, 27.3 an output. */ + .global silk_IIR_FIR_16k_armv4 + .type silk_IIR_FIR_16k_armv4, %function +silk_IIR_FIR_16k_armv4: + push {r4-r11, lr} + mov r10, #0x7f00 + orr r10, r10, #0xff @ 0x7fff, for the clamp + mov r11, #0x4000 @ the rounding bias + ldrsh r6, [r1, #2] @ x1 +.L16k_loop: + add r8, r6, r6, lsl #1 + ldrsh r7, [r1, #4] @ x2 + add r3, r11, r6, lsl #3 @ x1 * -600 + sub r3, r3, r6, lsl #9 + sub r3, r3, r8, lsl #5 + add r4, r11, r6, lsl #7 @ x1 * 896 + add r4, r4, r8, lsl #8 + sub r5, r11, r8, lsl #4 @ x1 * -48 + rsb r8, r7, r7, lsl #5 + ldrsh r6, [r1, #6] @ x3 + add r3, r3, r7 @ x2 * 617 + sub r3, r3, r7, lsl #2 + add r3, r3, r8, lsl #2 + add r3, r3, r8, lsl #4 + add r4, r4, r7, lsl #9 @ x2 * -3487 + sub r4, r4, r8 + sub r4, r4, r8, lsl #7 + sub r5, r5, r7, lsl #1 @ x2 * 758 + add r5, r5, r7, lsl #9 + add r5, r5, r8, lsl #3 + rsb r8, r6, r6, lsl #5 + ldrsh r7, [r1, #8] @ x4 + add r3, r3, r6, lsl #15 @ x3 * 30567 + add r3, r3, r8 + sub r3, r3, r8, lsl #3 + sub r3, r3, r8, lsl #6 + sub r4, r4, r6, lsl #4 @ x3 * 11950 + add r4, r4, r8, lsl #1 + add r4, r4, r8, lsl #7 + add r4, r4, r8, lsl #8 + add r5, r5, r6, lsl #2 @ x3 * -3956 + add r5, r5, r6, lsl #3 + sub r5, r5, r8, lsl #7 + add r8, r7, r7, lsl #1 + ldrsh r6, [r1, #10] @ x5 + rsb r9, r7, r7, lsl #6 + sub r3, r3, r7, lsl #6 @ x4 * 2996 + sub r3, r3, r8, lsl #2 + add r3, r3, r8, lsl #10 + add r4, r4, r7 @ x4 * 26341 + add r4, r4, r8, lsl #13 + sub r4, r4, r9, lsl #2 + add r4, r4, r9, lsl #5 + sub r5, r5, r8 @ x4 * 23973 + sub r5, r5, r8, lsl #5 + add r5, r5, r8, lsl #13 + sub r5, r5, r9, lsl #3 + add r8, r6, r6, lsl #1 + ldrsh r7, [r1, #12] @ x6 + add r9, r6, r6, lsl #8 + sub r3, r3, r8, lsl #5 @ x5 * -1375 + sub r3, r3, r8, lsl #9 + add r3, r3, r9 + sub r4, r4, r8, lsl #1 @ x5 * -3350 + add r4, r4, r8, lsl #8 + sub r4, r4, r9, lsl #4 + sub r5, r5, r6, lsl #10 @ x5 * 15143 + sub r5, r5, r8, lsl #3 + sub r5, r5, r9 + add r5, r5, r9, lsl #6 + add r8, r7, r7, lsl #4 + ldrsh r6, [r1, #14] @ x7 + add r3, r3, r8 @ x6 * 425 + add r3, r3, r8, lsl #3 + add r3, r3, r8, lsl #4 + add r4, r4, r7, lsl #9 @ x6 * 529 + add r4, r4, r8 + add r5, r5, r7 @ x6 * -3957 + add r5, r5, r7, lsl #1 + sub r5, r5, r7, lsl #12 + add r5, r5, r8, lsl #3 + rsb r8, r6, r6, lsl #3 + ldrsh r7, [r1, #16] @ x8 + sub r3, r3, r6, lsl #5 @ x7 * -46 + sub r3, r3, r8, lsl #1 + sub r4, r4, r6, lsl #2 @ x7 * -4 + sub r5, r5, r6 @ x7 * 967 + add r5, r5, r6, lsl #10 + sub r5, r5, r8, lsl #3 + add r5, r5, r7 @ x8 * -107 + ldrsh r6, [r1], #4 @ x0, and advance + add r5, r5, r7, lsl #2 + add r5, r5, r7, lsl #4 + sub r5, r5, r7, lsl #7 + add r8, r6, r6, lsl #1 + sub r3, r3, r8 @ x0 * 189 + add r3, r3, r8, lsl #6 + add r4, r4, r6 @ x0 * -103 + sub r4, r4, r6, lsl #3 + sub r4, r4, r8, lsl #5 + teq r3, r3, lsl #1 @ out of 16 bits? + ldrsh r6, [r1, #2] @ next x1 + mov r8, r3, asr #15 + eormi r8, r10, r3, asr #31 + strh r8, [r0], #2 + teq r4, r4, lsl #1 @ out of 16 bits? + mov r9, r4, asr #15 + eormi r9, r10, r4, asr #31 + strh r9, [r0], #2 + teq r5, r5, lsl #1 @ out of 16 bits? + mov r8, r5, asr #15 + eormi r8, r10, r5, asr #31 + strh r8, [r0], #2 + subs r2, r2, #1 + bne .L16k_loop + pop {r4-r11, pc} + .size silk_IIR_FIR_16k_armv4, .-silk_IIR_FIR_16k_armv4 + +/* 8k: phases 0/4/8, 8 samples a cycle, advancing 1. 79 shifted adds a + cycle, 26.3 an output. */ + .global silk_IIR_FIR_8k_armv4 + .type silk_IIR_FIR_8k_armv4, %function +silk_IIR_FIR_8k_armv4: + push {r4-r11, lr} + mov r10, #0x7f00 + orr r10, r10, #0xff @ 0x7fff, for the clamp + mov r11, #0x4000 @ the rounding bias + ldrsh r6, [r1, #2] @ x1 +.L8k_loop: + add r8, r6, r6, lsl #2 + ldrsh r7, [r1, #4] @ x2 + add r3, r11, r8, lsl #3 @ x1 * -600 + sub r3, r3, r8, lsl #7 + add r4, r11, r6, lsl #7 @ x1 * 758 + sub r4, r4, r8, lsl #1 + add r4, r4, r8, lsl #7 + sub r5, r11, r6, lsl #7 @ x1 * 896 + add r5, r5, r6, lsl #10 + add r8, r7, r7, lsl #1 + ldrsh r6, [r1, #6] @ x3 + rsb r9, r7, r7, lsl #5 + sub r3, r3, r8 @ x2 * 617 + add r3, r3, r9, lsl #2 + add r3, r3, r9, lsl #4 + add r4, r4, r8, lsl #2 @ x2 * -3956 + sub r4, r4, r9, lsl #7 + add r5, r5, r7, lsl #9 @ x2 * -3487 + sub r5, r5, r9 + sub r5, r5, r9, lsl #7 + rsb r8, r6, r6, lsl #4 + ldrsh r7, [r1, #8] @ x4 + add r9, r6, r6, lsl #5 + sub r3, r3, r8, lsl #3 @ x3 * 30567 + add r3, r3, r8, lsl #11 + sub r3, r3, r9 + add r4, r4, r8, lsl #10 @ x3 * 23973 + add r4, r4, r9 + add r4, r4, r9, lsl #2 + add r4, r4, r9, lsl #8 + add r5, r5, r6, lsl #4 @ x3 * 11950 + add r5, r5, r8, lsl #1 + add r5, r5, r8, lsl #9 + add r5, r5, r9, lsl #7 + add r8, r7, r7, lsl #1 + ldrsh r6, [r1, #10] @ x5 + rsb r9, r7, r7, lsl #3 + sub r3, r3, r7, lsl #6 @ x4 * 2996 + sub r3, r3, r8, lsl #2 + add r3, r3, r8, lsl #10 + add r4, r4, r7, lsl #5 @ x4 * 15143 + add r4, r4, r8, lsl #8 + add r4, r4, r9 + add r4, r4, r9, lsl #11 + add r5, r5, r7 @ x4 * 26341 + add r5, r5, r8, lsl #13 + sub r5, r5, r9, lsl #2 + add r5, r5, r9, lsl #8 + rsb r8, r6, r6, lsl #3 + ldrsh r7, [r1, #12] @ x6 + rsb r9, r6, r6, lsl #5 + add r3, r3, r8, lsl #6 @ x5 * -1375 + sub r3, r3, r8, lsl #8 + sub r3, r3, r9 + add r4, r4, r6, lsl #2 @ x5 * -3957 + add r4, r4, r8 + sub r4, r4, r9, lsl #7 + sub r5, r5, r8, lsl #1 @ x5 * -3350 + sub r5, r5, r8, lsl #9 + add r5, r5, r9, lsl #3 + rsb r8, r7, r7, lsl #3 + ldrsh r6, [r1, #14] @ x7 + sub r3, r3, r7, lsl #4 @ x6 * 425 + sub r3, r3, r8 + add r3, r3, r8, lsl #6 + sub r4, r4, r7 @ x6 * 967 + add r4, r4, r7, lsl #10 + sub r4, r4, r8, lsl #3 + add r5, r5, r7 @ x6 * 529 + add r5, r5, r7, lsl #4 + add r5, r5, r7, lsl #9 + add r8, r6, r6, lsl #1 + ldrsh r7, [r1], #2 @ x0, and advance + add r3, r3, r6, lsl #1 @ x7 * -46 + sub r3, r3, r8, lsl #4 + add r4, r4, r6 @ x7 * -107 + sub r4, r4, r8, lsl #2 + sub r4, r4, r8, lsl #5 + sub r5, r5, r6, lsl #2 @ x7 * -4 + add r8, r7, r7, lsl #1 + sub r3, r3, r8 @ x0 * 189 + add r3, r3, r8, lsl #6 + sub r4, r4, r8, lsl #4 @ x0 * -48 + add r5, r5, r7 @ x0 * -103 + sub r5, r5, r7, lsl #3 + sub r5, r5, r8, lsl #5 + teq r3, r3, lsl #1 @ out of 16 bits? + ldrsh r6, [r1, #2] @ next x1 + mov r8, r3, asr #15 + eormi r8, r10, r3, asr #31 + strh r8, [r0], #2 + teq r4, r4, lsl #1 @ out of 16 bits? + mov r9, r4, asr #15 + eormi r9, r10, r4, asr #31 + strh r9, [r0], #2 + teq r5, r5, lsl #1 @ out of 16 bits? + mov r8, r5, asr #15 + eormi r8, r10, r5, asr #31 + strh r8, [r0], #2 + subs r2, r2, #1 + bne .L8k_loop + pop {r4-r11, pc} + .size silk_IIR_FIR_8k_armv4, .-silk_IIR_FIR_8k_armv4 + +/* 12k: phases 0/6, 8 samples a cycle, advancing 1. 55 shifted adds a + cycle, 27.5 an output. */ + .global silk_IIR_FIR_12k_armv4 + .type silk_IIR_FIR_12k_armv4, %function +silk_IIR_FIR_12k_armv4: + push {r4-r11, lr} + mov r10, #0x7f00 + orr r10, r10, #0xff @ 0x7fff, for the clamp + mov r11, #0x4000 @ the rounding bias + ldrsh r6, [r1, #2] @ x1 +.L12k_loop: + add r8, r6, r6, lsl #2 + ldrsh r7, [r1, #4] @ x2 + add r3, r11, r8, lsl #3 @ x1 * -600 + sub r3, r3, r8, lsl #7 + sub r4, r11, r6, lsl #5 @ x1 * 972 + add r4, r4, r6, lsl #10 + sub r4, r4, r8, lsl #2 + rsb r8, r7, r7, lsl #3 + ldrsh r6, [r1, #6] @ x3 + add r3, r3, r7, lsl #9 @ x2 * 617 + sub r3, r3, r8 + add r3, r3, r8, lsl #4 + add r4, r4, r7, lsl #1 @ x2 * -4222 + sub r4, r4, r7, lsl #7 + sub r4, r4, r7, lsl #12 + add r8, r6, r6, lsl #3 + ldrsh r7, [r1, #8] @ x4 + sub r3, r3, r6, lsl #11 @ x3 * 30567 + add r3, r3, r6, lsl #15 + sub r3, r3, r8 + sub r3, r3, r8, lsl #4 + sub r4, r4, r6 @ x3 * 18278 + sub r4, r4, r8 + sub r4, r4, r8, lsl #4 + add r4, r4, r8, lsl #11 + add r8, r7, r7, lsl #1 + ldrsh r6, [r1, #10] @ x5 + sub r3, r3, r7, lsl #6 @ x4 * 2996 + sub r3, r3, r8, lsl #2 + add r3, r3, r8, lsl #10 + sub r4, r4, r7, lsl #8 @ x4 * 21254 + add r4, r4, r8, lsl #1 + sub r4, r4, r8, lsl #10 + add r4, r4, r8, lsl #13 + add r8, r6, r6, lsl #1 + ldrsh r7, [r1, #12] @ x6 + add r9, r6, r6, lsl #5 + add r3, r3, r6, lsl #7 @ x5 * -1375 + sub r3, r3, r8, lsl #9 + add r3, r3, r9 + add r4, r4, r6 @ x5 * -4235 + sub r4, r4, r8, lsl #2 + sub r4, r4, r9, lsl #7 + rsb r8, r7, r7, lsl #3 + ldrsh r6, [r1, #14] @ x7 + sub r3, r3, r7, lsl #4 @ x6 * 425 + sub r3, r3, r8 + add r3, r3, r8, lsl #6 + add r4, r4, r7 @ x6 * 905 + add r4, r4, r7, lsl #3 + add r4, r4, r8, lsl #7 + add r3, r3, r6, lsl #1 @ x7 * -46 + ldrsh r7, [r1], #2 @ x0, and advance + add r3, r3, r6, lsl #4 + sub r3, r3, r6, lsl #6 + sub r4, r4, r6, lsl #4 @ x7 * -80 + sub r4, r4, r6, lsl #6 + add r8, r7, r7, lsl #1 + sub r3, r3, r8 @ x0 * 189 + add r3, r3, r8, lsl #6 + sub r4, r4, r8 @ x0 * -99 + sub r4, r4, r8, lsl #5 + teq r3, r3, lsl #1 @ out of 16 bits? + ldrsh r6, [r1, #2] @ next x1 + mov r8, r3, asr #15 + eormi r8, r10, r3, asr #31 + strh r8, [r0], #2 + teq r4, r4, lsl #1 @ out of 16 bits? + mov r9, r4, asr #15 + eormi r9, r10, r4, asr #31 + strh r9, [r0], #2 + subs r2, r2, #1 + bne .L12k_loop + pop {r4-r11, pc} + .size silk_IIR_FIR_12k_armv4, .-silk_IIR_FIR_12k_armv4 diff --git a/lib/rbcodec/codecs/libopus/silk/resampler_private_IIR_FIR.c b/lib/rbcodec/codecs/libopus/silk/resampler_private_IIR_FIR.c index c09551d3a0..670280185f 100644 --- a/lib/rbcodec/codecs/libopus/silk/resampler_private_IIR_FIR.c +++ b/lib/rbcodec/codecs/libopus/silk/resampler_private_IIR_FIR.c @@ -108,6 +108,36 @@ static OPUS_INLINE opus_int16 *silk_IIR_FIR_cycle2( } return out; } + +#if defined(OPUS_ARM_ASM_ARMV4_ONLY) && !defined(OPUS_ARM_NO_SILK_ASM) +/* The same cycles as shifts and adds of constants, in + silk/arm/resampler_armv4_asm.S; each runs whole cycles only. */ +#define SILK_IIR_FIR_ASM +#define SILK_IIR_FIR_16K silk_IIR_FIR_16k_armv4, 43691, 3, 1, 2 +#define SILK_IIR_FIR_8K silk_IIR_FIR_8k_armv4, 21846, 3, 1, 1 +#define SILK_IIR_FIR_12K silk_IIR_FIR_12k_armv4, 32768, 2, 1, 1 +#endif + +#ifdef SILK_IIR_FIR_ASM +opus_int16 *silk_IIR_FIR_16k_armv4( opus_int16 *out, const opus_int16 *buf, opus_int32 iters ); +opus_int16 *silk_IIR_FIR_8k_armv4( opus_int16 *out, const opus_int16 *buf, opus_int32 iters ); +opus_int16 *silk_IIR_FIR_12k_armv4( opus_int16 *out, const opus_int16 *buf, opus_int32 iters ); + +/* Run the kernel over every whole iteration of cyc cycles of n outputs at + step inc, each advancing adv samples, and leave the C the rest. Cycle j + is whole when its last output's index, (j*n + n-1)*inc, is below + max_index_Q16. */ +#define SILK_IIR_FIR_RUN( args ) SILK_IIR_FIR_RUN_( args ) +#define SILK_IIR_FIR_RUN_( fn, inc, n, cyc, adv ) \ + do { \ + opus_int32 iters = ( max_index_Q16 + (inc) - 1 ) / ( (n) * (inc) ) / (cyc); \ + if( iters > 0 ) { \ + out = fn( out, buf, iters ); \ + buf += (adv) * iters; \ + max_index_Q16 -= iters * (cyc) * (n) * (inc); \ + } \ + } while( 0 ) +#endif #endif static OPUS_INLINE opus_int16 *silk_resampler_private_IIR_FIR_INTERPOL( @@ -124,10 +154,19 @@ static OPUS_INLINE opus_int16 *silk_resampler_private_IIR_FIR_INTERPOL( #ifndef OPUS_NO_SILK_FIXED_PHASE switch( index_increment_Q16 ) { case 43691: /* 16 kHz */ +#ifdef SILK_IIR_FIR_ASM + SILK_IIR_FIR_RUN( SILK_IIR_FIR_16K ); +#endif return silk_IIR_FIR_cycle3( out, buf, max_index_Q16, 43691, 0, 8, 4, 1, 2 ); case 21846: /* 8 kHz */ +#ifdef SILK_IIR_FIR_ASM + SILK_IIR_FIR_RUN( SILK_IIR_FIR_8K ); +#endif return silk_IIR_FIR_cycle3( out, buf, max_index_Q16, 21846, 0, 4, 8, 0, 1 ); case 32768: /* 12 kHz */ +#ifdef SILK_IIR_FIR_ASM + SILK_IIR_FIR_RUN( SILK_IIR_FIR_12K ); +#endif return silk_IIR_FIR_cycle2( out, buf, max_index_Q16 ); } #endif From deaef503bbc5080596677d845933cf1d98beb2d3 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 28 Sep 2026 21:36:21 -0400 Subject: [PATCH 280/408] opus: ARMv5E SILK resampler interpolation On ARMv5E and later the fixed-phase FIR cycles at 8, 12 and 16 kHz load two samples a word and take two coefficients a word from a literal pool; smla picks the halves, so an odd-aligned window costs nothing. Outputs are paired as two interleaved accumulator chains. The FIR buffer is now word aligned. Generated by silk/arm/gen_resampler_armv5e.py. Bit-exact; OPUS_ARM_NO_SILK_ASM disables it, and config.h sets that on M-profile cores. Measured on the Clip+, silk_5.opus (WB SILK): 20.93 -> 15.84 MHz, -24.3%. Co-Authored-By: Claude Opus 5.5 Change-Id: Ibec71fd52542f768f22e2970c9f8c45118c708b9 --- lib/rbcodec/codecs/libopus/README.rockbox | 7 +- lib/rbcodec/codecs/libopus/SOURCES | 1 + lib/rbcodec/codecs/libopus/config.h | 1 + .../libopus/silk/arm/gen_resampler_armv5e.py | 224 ++++++++++++ .../libopus/silk/arm/resampler_armv5e_asm.S | 321 ++++++++++++++++++ .../libopus/silk/resampler_private_IIR_FIR.c | 23 ++ 6 files changed, 574 insertions(+), 3 deletions(-) create mode 100644 lib/rbcodec/codecs/libopus/silk/arm/gen_resampler_armv5e.py create mode 100644 lib/rbcodec/codecs/libopus/silk/arm/resampler_armv5e_asm.S diff --git a/lib/rbcodec/codecs/libopus/README.rockbox b/lib/rbcodec/codecs/libopus/README.rockbox index 9c6eefe4a9..b63fae2251 100644 --- a/lib/rbcodec/codecs/libopus/README.rockbox +++ b/lib/rbcodec/codecs/libopus/README.rockbox @@ -21,9 +21,10 @@ Celt: Silk: * added fixed-phase interpolation paths for the 8, 12 and 16 kHz to 48 kHz steps in resampler_private_IIR_FIR.c (disable with OPUS_NO_SILK_FIXED_PHASE) -* resampler_private_IIR_FIR.c calls an ARMv4 kernel for those three steps, - silk/arm/resampler_armv4_asm.S, generated by gen_resampler_armv4.py beside - it (disable with OPUS_ARM_NO_SILK_ASM) +* resampler_private_IIR_FIR.c calls ARM kernels for those three steps: + silk/arm/resampler_armv4_asm.S and resampler_armv5e_asm.S, generated by + gen_resampler_armv4.py and gen_resampler_armv5e.py beside them (disable with + OPUS_ARM_NO_SILK_ASM); on ARMv5E and later the FIR buffer is word aligned Opus-tools: * copied src/opus_header.h and src/opus_header.c to lib/rbcodec/codecs/libopus diff --git a/lib/rbcodec/codecs/libopus/SOURCES b/lib/rbcodec/codecs/libopus/SOURCES index bacf07dd86..379f700cbe 100644 --- a/lib/rbcodec/codecs/libopus/SOURCES +++ b/lib/rbcodec/codecs/libopus/SOURCES @@ -28,6 +28,7 @@ celt/arm/exp_rotation1_armv5e_asm.S celt/arm/haar1_armv5e_asm.S celt/arm/normres_armv5e_asm.S celt/arm/deemph_armv5e_asm.S +silk/arm/resampler_armv5e_asm.S #endif celt/laplace.c celt/mathops.c diff --git a/lib/rbcodec/codecs/libopus/config.h b/lib/rbcodec/codecs/libopus/config.h index e3c9b0f299..43cd6019bf 100644 --- a/lib/rbcodec/codecs/libopus/config.h +++ b/lib/rbcodec/codecs/libopus/config.h @@ -91,6 +91,7 @@ #define OPUS_NO_PFA #define OPUS_ARM_NO_COMB_ASM #define OPUS_ARM_NO_BANDS_ASM +#define OPUS_ARM_NO_SILK_ASM #endif #endif /* ARM_ARCH */ diff --git a/lib/rbcodec/codecs/libopus/silk/arm/gen_resampler_armv5e.py b/lib/rbcodec/codecs/libopus/silk/arm/gen_resampler_armv5e.py new file mode 100644 index 0000000000..ae29853b90 --- /dev/null +++ b/lib/rbcodec/codecs/libopus/silk/arm/gen_resampler_armv5e.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +"""Generate silk/arm/resampler_armv5e_asm.S. + +The FIR half of silk_resampler_private_IIR_FIR at 8, 12 and 16 kHz, for +ARMv5E and later. Unlike ARMv4 (gen_resampler_armv4.py, beside this one), +the multiplier is the cheap path here: smla is a 16x16 +multiply-accumulate that issues in one cycle and picks either half of each +operand, so two samples arrive in one word, two coefficients in another, and +nothing is unpacked. + + python3 gen_resampler_armv5e.py > resampler_armv5e_asm.S +""" +import sys + +T = [[189, -600, 617, 30567], [117, -159, -1070, 29704], + [52, 221, -2392, 28276], [-4, 529, -3350, 26341], + [-48, 758, -3956, 23973], [-80, 905, -4235, 21254], + [-99, 972, -4222, 18278], [-107, 967, -3957, 15143], + [-103, 896, -3487, 11950], [-91, 773, -2865, 8798], + [-71, 611, -2143, 5784], [-46, 425, -1375, 2996]] +P = {p: T[p] + T[11 - p][::-1] for p in range(12)} + +# Rates: name, outputs per loop iteration as (phase, first sample), and the +# words the window advances per iteration. The buffer is word aligned, so +# sample s is half s&1 of word s>>1. 8 and 12 kHz advance one sample a +# cycle, so an iteration is two cycles and advances one word. +RATES = [ + ("16k", [(0, 0), (8, 0), (4, 1)], 1), + ("8k", [(0, 0), (4, 0), (8, 0), (0, 1), (4, 1), (8, 1)], 1), + ("12k", [(0, 0), (6, 0), (0, 1), (6, 1)], 1), +] + +W = ["r5", "r6", "r7", "r8", "r9"] # sample words W0..W4 +CP = ["r10", "r12"] # coefficient words, first chain +CQ = ["r11", "lr"] # and second + + +def half(h): + return "t" if h else "b" + + +def coef_words(p): + """The four words of phase p's taps, low half the even tap.""" + c = P[p] + return [((c[2 * i + 1] & 0xffff) << 16) | (c[2 * i] & 0xffff) + for i in range(4)] + + +class Pool: + def __init__(self, name): + self.name, self.words = name, [] + + def label(self, w): + if w not in self.words: + self.words.append(w) + return ".L%s_k%d" % (self.name, self.words.index(w)) + + +def mac(acc, tap, first, off, cw, bias): + """smla/smul for tap `tap` of an output whose window starts at off.""" + s = off + tap + x, y = half(s & 1), half(tap & 1) + if first and not bias: + return "smul%s%s %s, %s, %s" % (x, y, acc, W[s >> 1], cw) + return "smla%s%s %s, %s, %s, %s" % (x, y, acc, W[s >> 1], cw, acc) + + +def chains(pool, a, b, first_loads_done): + """Two interleaved multiply-accumulate chains. + + a and b are (acc, [(tap, off, word)], bias): each word covers two taps. + Returns the instructions; the first word of each chain is expected to + be loaded already into CP[0] and CQ[0]. + """ + out = [] + (acc_a, wa, bias_a), (acc_b, wb, bias_b) = a, b + n = max(len(wa), len(wb)) + for w in range(n): + ra, rb = CP[w % 2], CQ[w % 2] + na, nb = CP[(w + 1) % 2], CQ[(w + 1) % 2] + if w < len(wa): + tap, off, _ = wa[w] + out.append(mac(acc_a, tap, w == 0, off, ra, bias_a)) + if w < len(wb): + tap, off, _ = wb[w] + out.append(mac(acc_b, tap, w == 0, off, rb, bias_b)) + if w + 1 < len(wa): + out.append("ldr %s, %s" % (na, pool.label(wa[w + 1][2]))) + if w < len(wa): + tap, off, _ = wa[w] + out.append(mac(acc_a, tap + 1, False, off, ra, bias_a)) + if w + 1 < len(wb): + out.append("ldr %s, %s" % (nb, pool.label(wb[w + 1][2]))) + if w < len(wb): + tap, off, _ = wb[w] + out.append(mac(acc_b, tap + 1, False, off, rb, bias_b)) + return out + + +def gen_rate(name, outs, adv, f): + pool = Pool(name) + fn = "silk_IIR_FIR_%s_armv5e" % name + w = f.write + w("\n/* %s: %d outputs an iteration, phases %s. */\n" + % (name, len(outs), "/".join(str(p) for p, _ in outs))) + w(" .global %s\n .type %s, %%function\n%s:\n" % (fn, fn, fn)) + w(" push {r4-r11, lr}\n") + w(".L%s_loop:\n" % name) + + # Group outputs in pairs, two chains each; an odd one out is split into + # its first and last four taps and the halves summed. + groups = [] + i = 0 + while i < len(outs): + if i + 1 < len(outs): + groups.append(("pair", outs[i], outs[i + 1])) + i += 2 + else: + groups.append(("split", outs[i])) + i += 1 + + def spec(p, off): + return [(2 * k, off, cw) for k, cw in enumerate(coef_words(p))] + + body = [] + body.append("ldr %s, [r1], #%d" % (W[0], 4 * adv)) + body.append("ldmia r1, {%s-%s}" % (W[1], W[4])) + loaded = False + for g in groups: + if g[0] == "pair": + (pa, oa), (pb, ob) = g[1], g[2] + a = ("r3", spec(pa, oa), True) + b = ("r4", spec(pb, ob), True) + desc = ["phase %d at x%d" % (pa, oa), "phase %d at x%d" % (pb, ob)] + else: + p, o = g[1] + s = spec(p, o) + a = ("r3", s[:2], True) + b = ("r4", s[2:], False) + desc = ["phase %d at x%d, taps 0-3" % (p, o), "taps 4-7"] + pre = ["ldr %s, %s" % (CP[0], pool.label(a[1][0][2])), + "ldr %s, %s" % (CQ[0], pool.label(b[1][0][2])), + "mov r3, #0x4000 @ rounding bias"] + if b[2]: + pre.append("mov r4, #0x4000") + pre[0] += " " * max(1, 36 - len(pre[0])) + "@ " + desc[0] + pre[1] += " " * max(1, 36 - len(pre[1])) + "@ " + desc[1] + body += pre + body += chains(pool, a, b, loaded) + if g[0] == "pair": + body += ["qadd r3, r3, r3 @ SAT16(x >> 15)", + "qadd r4, r4, r4 @ in the top half", + "mov r3, r3, asr #16", + "mov r4, r4, asr #16", + "strh r3, [r0], #2", + "strh r4, [r0], #2"] + else: + body += ["add r3, r3, r4", + "qadd r3, r3, r3 @ SAT16(x >> 15)", + "mov r3, r3, asr #16", + "strh r3, [r0], #2"] + body += ["subs r2, r2, #1", "bne .L%s_loop" % name, + "pop {r4-r11, pc}"] + for b in body: + w(" " + b + "\n") + w(" .align 2\n") + for k, v in enumerate(pool.words): + lo, hi = v & 0xffff, v >> 16 + sg = lambda h: h - 0x10000 if h & 0x8000 else h + w(".L%s_k%d: .word 0x%08x @ %d, %d\n" + % (name, k, v, sg(lo), sg(hi))) + w(" .size %s, .-%s\n" % (fn, fn)) + + +HEAD = """/* Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * silk/resampler_private_IIR_FIR.c are met. + */ + +/* GENERATED by silk/arm/gen_resampler_armv5e.py -- edit that, not this. + * + * ARMv5E FIR interpolation for silk_resampler_private_IIR_FIR at the three + * SILK rates. Each step visits two or three fixed phase filters in a fixed + * cycle (see the C), so the coefficients are constants, packed two to a word + * in each function's literal pool. The buffer is word aligned, so one load + * brings two samples and smla takes whichever halves a tap needs: + * a window starting on an odd sample costs nothing extra. + * + * Outputs are computed in pairs, as two interleaved multiply-accumulate + * chains, so no smla waits on the one before it. An odd output out splits + * its eight taps across the two chains and sums them. The rounding bias + * starts each accumulator, and qadd doubles with saturation, so the top half + * is exactly SAT16((sum + 2^14) >> 15). + * + * opus_int16 *silk_IIR_FIR__armv5e(opus_int16 *out, + * const opus_int16 *buf, int iters) + * + * buf must be word aligned. Runs `iters` whole iterations (iters > 0) -- + * one cycle at 16 kHz, two at 8 and 12 kHz -- and returns the advanced out. + * Each iteration reads the ten samples from buf and advances it one word. + * r0 out, r1 buf, r2 iters, r3/r4 accumulators, r5-r9 sample words, + * r10/r12 and r11/lr coefficient words for the two chains. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "resampler_armv5e_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 +""" + + +def main(): + f = sys.stdout + f.write(HEAD) + for r in RATES: + gen_rate(*r, f=f) + + +if __name__ == "__main__": + main() diff --git a/lib/rbcodec/codecs/libopus/silk/arm/resampler_armv5e_asm.S b/lib/rbcodec/codecs/libopus/silk/arm/resampler_armv5e_asm.S new file mode 100644 index 0000000000..99c235295a --- /dev/null +++ b/lib/rbcodec/codecs/libopus/silk/arm/resampler_armv5e_asm.S @@ -0,0 +1,321 @@ +/* Copyright (c) 2026 Michael Giacomelli + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the conditions stated in + * silk/resampler_private_IIR_FIR.c are met. + */ + +/* GENERATED by silk/arm/gen_resampler_armv5e.py -- edit that, not this. + * + * ARMv5E FIR interpolation for silk_resampler_private_IIR_FIR at the three + * SILK rates. Each step visits two or three fixed phase filters in a fixed + * cycle (see the C), so the coefficients are constants, packed two to a word + * in each function's literal pool. The buffer is word aligned, so one load + * brings two samples and smla takes whichever halves a tap needs: + * a window starting on an odd sample costs nothing extra. + * + * Outputs are computed in pairs, as two interleaved multiply-accumulate + * chains, so no smla waits on the one before it. An odd output out splits + * its eight taps across the two chains and sums them. The rounding bias + * starts each accumulator, and qadd doubles with saturation, so the top half + * is exactly SAT16((sum + 2^14) >> 15). + * + * opus_int16 *silk_IIR_FIR__armv5e(opus_int16 *out, + * const opus_int16 *buf, int iters) + * + * buf must be word aligned. Runs `iters` whole iterations (iters > 0) -- + * one cycle at 16 kHz, two at 8 and 12 kHz -- and returns the advanced out. + * Each iteration reads the ten samples from buf and advances it one word. + * r0 out, r1 buf, r2 iters, r3/r4 accumulators, r5-r9 sample words, + * r10/r12 and r11/lr coefficient words for the two chains. + */ + +#if defined(__thumb__) || defined(__thumb2__) +#error "resampler_armv5e_asm.S must be assembled in ARM mode" +#endif + + .text + .align 2 + +/* 16k: 3 outputs an iteration, phases 0/8/4. */ + .global silk_IIR_FIR_16k_armv5e + .type silk_IIR_FIR_16k_armv5e, %function +silk_IIR_FIR_16k_armv5e: + push {r4-r11, lr} +.L16k_loop: + ldr r5, [r1], #4 + ldmia r1, {r6-r9} + ldr r10, .L16k_k0 @ phase 0 at x0 + ldr r11, .L16k_k1 @ phase 8 at x0 + mov r3, #0x4000 @ rounding bias + mov r4, #0x4000 + smlabb r3, r5, r10, r3 + smlabb r4, r5, r11, r4 + ldr r12, .L16k_k2 + smlatt r3, r5, r10, r3 + ldr lr, .L16k_k3 + smlatt r4, r5, r11, r4 + smlabb r3, r6, r12, r3 + smlabb r4, r6, lr, r4 + ldr r10, .L16k_k4 + smlatt r3, r6, r12, r3 + ldr r11, .L16k_k5 + smlatt r4, r6, lr, r4 + smlabb r3, r7, r10, r3 + smlabb r4, r7, r11, r4 + ldr r12, .L16k_k6 + smlatt r3, r7, r10, r3 + ldr lr, .L16k_k7 + smlatt r4, r7, r11, r4 + smlabb r3, r8, r12, r3 + smlabb r4, r8, lr, r4 + smlatt r3, r8, r12, r3 + smlatt r4, r8, lr, r4 + qadd r3, r3, r3 @ SAT16(x >> 15) + qadd r4, r4, r4 @ in the top half + mov r3, r3, asr #16 + mov r4, r4, asr #16 + strh r3, [r0], #2 + strh r4, [r0], #2 + ldr r10, .L16k_k8 @ phase 4 at x1, taps 0-3 + ldr r11, .L16k_k9 @ taps 4-7 + mov r3, #0x4000 @ rounding bias + smlatb r3, r5, r10, r3 + smultb r4, r7, r11 + ldr r12, .L16k_k10 + smlabt r3, r6, r10, r3 + ldr lr, .L16k_k11 + smlabt r4, r8, r11, r4 + smlatb r3, r6, r12, r3 + smlatb r4, r8, lr, r4 + smlabt r3, r7, r12, r3 + smlabt r4, r9, lr, r4 + add r3, r3, r4 + qadd r3, r3, r3 @ SAT16(x >> 15) + mov r3, r3, asr #16 + strh r3, [r0], #2 + subs r2, r2, #1 + bne .L16k_loop + pop {r4-r11, pc} + .align 2 +.L16k_k0: .word 0xfda800bd @ 189, -600 +.L16k_k1: .word 0x0380ff99 @ -103, 896 +.L16k_k2: .word 0x77670269 @ 617, 30567 +.L16k_k3: .word 0x2eaef261 @ -3487, 11950 +.L16k_k4: .word 0xfaa10bb4 @ 2996, -1375 +.L16k_k5: .word 0xf2ea66e5 @ 26341, -3350 +.L16k_k6: .word 0xffd201a9 @ 425, -46 +.L16k_k7: .word 0xfffc0211 @ 529, -4 +.L16k_k8: .word 0x02f6ffd0 @ -48, 758 +.L16k_k9: .word 0xf08b3b27 @ 15143, -3957 +.L16k_k10: .word 0x5da5f08c @ -3956, 23973 +.L16k_k11: .word 0xff9503c7 @ 967, -107 + .size silk_IIR_FIR_16k_armv5e, .-silk_IIR_FIR_16k_armv5e + +/* 8k: 6 outputs an iteration, phases 0/4/8/0/4/8. */ + .global silk_IIR_FIR_8k_armv5e + .type silk_IIR_FIR_8k_armv5e, %function +silk_IIR_FIR_8k_armv5e: + push {r4-r11, lr} +.L8k_loop: + ldr r5, [r1], #4 + ldmia r1, {r6-r9} + ldr r10, .L8k_k0 @ phase 0 at x0 + ldr r11, .L8k_k1 @ phase 4 at x0 + mov r3, #0x4000 @ rounding bias + mov r4, #0x4000 + smlabb r3, r5, r10, r3 + smlabb r4, r5, r11, r4 + ldr r12, .L8k_k2 + smlatt r3, r5, r10, r3 + ldr lr, .L8k_k3 + smlatt r4, r5, r11, r4 + smlabb r3, r6, r12, r3 + smlabb r4, r6, lr, r4 + ldr r10, .L8k_k4 + smlatt r3, r6, r12, r3 + ldr r11, .L8k_k5 + smlatt r4, r6, lr, r4 + smlabb r3, r7, r10, r3 + smlabb r4, r7, r11, r4 + ldr r12, .L8k_k6 + smlatt r3, r7, r10, r3 + ldr lr, .L8k_k7 + smlatt r4, r7, r11, r4 + smlabb r3, r8, r12, r3 + smlabb r4, r8, lr, r4 + smlatt r3, r8, r12, r3 + smlatt r4, r8, lr, r4 + qadd r3, r3, r3 @ SAT16(x >> 15) + qadd r4, r4, r4 @ in the top half + mov r3, r3, asr #16 + mov r4, r4, asr #16 + strh r3, [r0], #2 + strh r4, [r0], #2 + ldr r10, .L8k_k8 @ phase 8 at x0 + ldr r11, .L8k_k0 @ phase 0 at x1 + mov r3, #0x4000 @ rounding bias + mov r4, #0x4000 + smlabb r3, r5, r10, r3 + smlatb r4, r5, r11, r4 + ldr r12, .L8k_k9 + smlatt r3, r5, r10, r3 + ldr lr, .L8k_k2 + smlabt r4, r6, r11, r4 + smlabb r3, r6, r12, r3 + smlatb r4, r6, lr, r4 + ldr r10, .L8k_k10 + smlatt r3, r6, r12, r3 + ldr r11, .L8k_k4 + smlabt r4, r7, lr, r4 + smlabb r3, r7, r10, r3 + smlatb r4, r7, r11, r4 + ldr r12, .L8k_k11 + smlatt r3, r7, r10, r3 + ldr lr, .L8k_k6 + smlabt r4, r8, r11, r4 + smlabb r3, r8, r12, r3 + smlatb r4, r8, lr, r4 + smlatt r3, r8, r12, r3 + smlabt r4, r9, lr, r4 + qadd r3, r3, r3 @ SAT16(x >> 15) + qadd r4, r4, r4 @ in the top half + mov r3, r3, asr #16 + mov r4, r4, asr #16 + strh r3, [r0], #2 + strh r4, [r0], #2 + ldr r10, .L8k_k1 @ phase 4 at x1 + ldr r11, .L8k_k8 @ phase 8 at x1 + mov r3, #0x4000 @ rounding bias + mov r4, #0x4000 + smlatb r3, r5, r10, r3 + smlatb r4, r5, r11, r4 + ldr r12, .L8k_k3 + smlabt r3, r6, r10, r3 + ldr lr, .L8k_k9 + smlabt r4, r6, r11, r4 + smlatb r3, r6, r12, r3 + smlatb r4, r6, lr, r4 + ldr r10, .L8k_k5 + smlabt r3, r7, r12, r3 + ldr r11, .L8k_k10 + smlabt r4, r7, lr, r4 + smlatb r3, r7, r10, r3 + smlatb r4, r7, r11, r4 + ldr r12, .L8k_k7 + smlabt r3, r8, r10, r3 + ldr lr, .L8k_k11 + smlabt r4, r8, r11, r4 + smlatb r3, r8, r12, r3 + smlatb r4, r8, lr, r4 + smlabt r3, r9, r12, r3 + smlabt r4, r9, lr, r4 + qadd r3, r3, r3 @ SAT16(x >> 15) + qadd r4, r4, r4 @ in the top half + mov r3, r3, asr #16 + mov r4, r4, asr #16 + strh r3, [r0], #2 + strh r4, [r0], #2 + subs r2, r2, #1 + bne .L8k_loop + pop {r4-r11, pc} + .align 2 +.L8k_k0: .word 0xfda800bd @ 189, -600 +.L8k_k1: .word 0x02f6ffd0 @ -48, 758 +.L8k_k2: .word 0x77670269 @ 617, 30567 +.L8k_k3: .word 0x5da5f08c @ -3956, 23973 +.L8k_k4: .word 0xfaa10bb4 @ 2996, -1375 +.L8k_k5: .word 0xf08b3b27 @ 15143, -3957 +.L8k_k6: .word 0xffd201a9 @ 425, -46 +.L8k_k7: .word 0xff9503c7 @ 967, -107 +.L8k_k8: .word 0x0380ff99 @ -103, 896 +.L8k_k9: .word 0x2eaef261 @ -3487, 11950 +.L8k_k10: .word 0xf2ea66e5 @ 26341, -3350 +.L8k_k11: .word 0xfffc0211 @ 529, -4 + .size silk_IIR_FIR_8k_armv5e, .-silk_IIR_FIR_8k_armv5e + +/* 12k: 4 outputs an iteration, phases 0/6/0/6. */ + .global silk_IIR_FIR_12k_armv5e + .type silk_IIR_FIR_12k_armv5e, %function +silk_IIR_FIR_12k_armv5e: + push {r4-r11, lr} +.L12k_loop: + ldr r5, [r1], #4 + ldmia r1, {r6-r9} + ldr r10, .L12k_k0 @ phase 0 at x0 + ldr r11, .L12k_k1 @ phase 6 at x0 + mov r3, #0x4000 @ rounding bias + mov r4, #0x4000 + smlabb r3, r5, r10, r3 + smlabb r4, r5, r11, r4 + ldr r12, .L12k_k2 + smlatt r3, r5, r10, r3 + ldr lr, .L12k_k3 + smlatt r4, r5, r11, r4 + smlabb r3, r6, r12, r3 + smlabb r4, r6, lr, r4 + ldr r10, .L12k_k4 + smlatt r3, r6, r12, r3 + ldr r11, .L12k_k5 + smlatt r4, r6, lr, r4 + smlabb r3, r7, r10, r3 + smlabb r4, r7, r11, r4 + ldr r12, .L12k_k6 + smlatt r3, r7, r10, r3 + ldr lr, .L12k_k7 + smlatt r4, r7, r11, r4 + smlabb r3, r8, r12, r3 + smlabb r4, r8, lr, r4 + smlatt r3, r8, r12, r3 + smlatt r4, r8, lr, r4 + qadd r3, r3, r3 @ SAT16(x >> 15) + qadd r4, r4, r4 @ in the top half + mov r3, r3, asr #16 + mov r4, r4, asr #16 + strh r3, [r0], #2 + strh r4, [r0], #2 + ldr r10, .L12k_k0 @ phase 0 at x1 + ldr r11, .L12k_k1 @ phase 6 at x1 + mov r3, #0x4000 @ rounding bias + mov r4, #0x4000 + smlatb r3, r5, r10, r3 + smlatb r4, r5, r11, r4 + ldr r12, .L12k_k2 + smlabt r3, r6, r10, r3 + ldr lr, .L12k_k3 + smlabt r4, r6, r11, r4 + smlatb r3, r6, r12, r3 + smlatb r4, r6, lr, r4 + ldr r10, .L12k_k4 + smlabt r3, r7, r12, r3 + ldr r11, .L12k_k5 + smlabt r4, r7, lr, r4 + smlatb r3, r7, r10, r3 + smlatb r4, r7, r11, r4 + ldr r12, .L12k_k6 + smlabt r3, r8, r10, r3 + ldr lr, .L12k_k7 + smlabt r4, r8, r11, r4 + smlatb r3, r8, r12, r3 + smlatb r4, r8, lr, r4 + smlabt r3, r9, r12, r3 + smlabt r4, r9, lr, r4 + qadd r3, r3, r3 @ SAT16(x >> 15) + qadd r4, r4, r4 @ in the top half + mov r3, r3, asr #16 + mov r4, r4, asr #16 + strh r3, [r0], #2 + strh r4, [r0], #2 + subs r2, r2, #1 + bne .L12k_loop + pop {r4-r11, pc} + .align 2 +.L12k_k0: .word 0xfda800bd @ 189, -600 +.L12k_k1: .word 0x03ccff9d @ -99, 972 +.L12k_k2: .word 0x77670269 @ 617, 30567 +.L12k_k3: .word 0x4766ef82 @ -4222, 18278 +.L12k_k4: .word 0xfaa10bb4 @ 2996, -1375 +.L12k_k5: .word 0xef755306 @ 21254, -4235 +.L12k_k6: .word 0xffd201a9 @ 425, -46 +.L12k_k7: .word 0xffb00389 @ 905, -80 + .size silk_IIR_FIR_12k_armv5e, .-silk_IIR_FIR_12k_armv5e diff --git a/lib/rbcodec/codecs/libopus/silk/resampler_private_IIR_FIR.c b/lib/rbcodec/codecs/libopus/silk/resampler_private_IIR_FIR.c index 670280185f..5f8acdf374 100644 --- a/lib/rbcodec/codecs/libopus/silk/resampler_private_IIR_FIR.c +++ b/lib/rbcodec/codecs/libopus/silk/resampler_private_IIR_FIR.c @@ -116,12 +116,24 @@ static OPUS_INLINE opus_int16 *silk_IIR_FIR_cycle2( #define SILK_IIR_FIR_16K silk_IIR_FIR_16k_armv4, 43691, 3, 1, 2 #define SILK_IIR_FIR_8K silk_IIR_FIR_8k_armv4, 21846, 3, 1, 1 #define SILK_IIR_FIR_12K silk_IIR_FIR_12k_armv4, 32768, 2, 1, 1 +#elif defined(OPUS_ARM_ASM_ARMV5E_AND_LATER) && !defined(OPUS_ARM_NO_SILK_ASM) +/* Packed 16x16 multiply-accumulates, in silk/arm/resampler_armv5e_asm.S. + Each loads two samples a word, so the buffer must be word aligned, and + at 8 and 12 kHz an iteration is two cycles, one word further on. */ +#define SILK_IIR_FIR_ASM +#define SILK_IIR_FIR_ALIGNED +#define SILK_IIR_FIR_16K silk_IIR_FIR_16k_armv5e, 43691, 3, 1, 2 +#define SILK_IIR_FIR_8K silk_IIR_FIR_8k_armv5e, 21846, 3, 2, 2 +#define SILK_IIR_FIR_12K silk_IIR_FIR_12k_armv5e, 32768, 2, 2, 2 #endif #ifdef SILK_IIR_FIR_ASM opus_int16 *silk_IIR_FIR_16k_armv4( opus_int16 *out, const opus_int16 *buf, opus_int32 iters ); opus_int16 *silk_IIR_FIR_8k_armv4( opus_int16 *out, const opus_int16 *buf, opus_int32 iters ); opus_int16 *silk_IIR_FIR_12k_armv4( opus_int16 *out, const opus_int16 *buf, opus_int32 iters ); +opus_int16 *silk_IIR_FIR_16k_armv5e( opus_int16 *out, const opus_int16 *buf, opus_int32 iters ); +opus_int16 *silk_IIR_FIR_8k_armv5e( opus_int16 *out, const opus_int16 *buf, opus_int32 iters ); +opus_int16 *silk_IIR_FIR_12k_armv5e( opus_int16 *out, const opus_int16 *buf, opus_int32 iters ); /* Run the kernel over every whole iteration of cyc cycles of n outputs at step inc, each advancing adv samples, and leave the C the rest. Cycle j @@ -199,10 +211,21 @@ void silk_resampler_private_IIR_FIR( silk_resampler_state_struct *S = (silk_resampler_state_struct *)SS; opus_int32 nSamplesIn; opus_int32 max_index_Q16, index_increment_Q16; +#ifdef SILK_IIR_FIR_ALIGNED + VARDECL( opus_int16, buf_alloc ); + opus_int16 *buf; +#else VARDECL( opus_int16, buf ); +#endif SAVE_STACK; +#ifdef SILK_IIR_FIR_ALIGNED + /* One sample spare, to start on a word */ + ALLOC( buf_alloc, 2 * S->batchSize + RESAMPLER_ORDER_FIR_12 + 1, opus_int16 ); + buf = buf_alloc + ( ( (size_t)buf_alloc >> 1 ) & 1 ); +#else ALLOC( buf, 2 * S->batchSize + RESAMPLER_ORDER_FIR_12, opus_int16 ); +#endif /* Copy buffered samples to start of buffer */ silk_memcpy( buf, S->sFIR.i16, RESAMPLER_ORDER_FIR_12 * sizeof( opus_int16 ) ); From 330236911b0d091d7bddfe9fe43ef7f5cac094aa Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Sat, 19 Sep 2026 16:16:45 -0400 Subject: [PATCH 281/408] flac: decode residuals that use all 32 bits The folded Rice value was unfolded with a signed shift, which is wrong once it reaches 2^31, and the unary length limit was (INT_MAX >> k) + 2, about half of what a 32-bit residual can need. Streams with very large residuals (FLAC decoder testbench file 63) were misparsed, overran the frame and lost sync at the next one. Unfold as unsigned and derive the limit from UINT_MAX, clamped to INT_MAX. The fast path is unchanged. The existing 0x80000000 error check now also works as intended, since the Golomb reader's error value maps to it. The FLAC spec forbids a residual of -2^31. Co-Authored-By: Claude Sonnet 5 Change-Id: I0bdce9db69e1b30565ecc23126923ba401b7deca --- lib/rbcodec/codecs/libffmpegFLAC/decoder.c | 3 ++- lib/rbcodec/codecs/libffmpegFLAC/golomb.h | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/rbcodec/codecs/libffmpegFLAC/decoder.c b/lib/rbcodec/codecs/libffmpegFLAC/decoder.c index dc5a98cf5b..9701a598b2 100644 --- a/lib/rbcodec/codecs/libffmpegFLAC/decoder.c +++ b/lib/rbcodec/codecs/libffmpegFLAC/decoder.c @@ -165,7 +165,8 @@ static int decode_residuals(FLACContext *s, int32_t *decoded, int pred_order) for (; i < samples; i++) *decoded++ = tmp ? get_sbits(&gb, tmp) : 0; } else { - int real_limit = tmp ? (INT_MAX >> tmp) + 2 : INT_MAX; + unsigned lim = tmp ? (UINT_MAX >> tmp) + 2 : INT_MAX; + int real_limit = lim > INT_MAX ? INT_MAX : lim; for (; i < samples; i++) { int v = get_sr_golomb_flac(&gb, tmp, real_limit, 0); if ((unsigned) v == 0x80000000){ diff --git a/lib/rbcodec/codecs/libffmpegFLAC/golomb.h b/lib/rbcodec/codecs/libffmpegFLAC/golomb.h index 68d2558e1d..b62051ff1a 100644 --- a/lib/rbcodec/codecs/libffmpegFLAC/golomb.h +++ b/lib/rbcodec/codecs/libffmpegFLAC/golomb.h @@ -85,7 +85,7 @@ static inline int get_ur_golomb_jpegls(GetBitContext *gb, int k, int limit, buf = 0; } - buf += ((int32_t)i << k); + buf += ((unsigned int)i << k); } else if (i == limit - 1) { buf = SHOW_UBITS(re, gb, esc_len); LAST_SKIP_BITS(re, gb, esc_len); @@ -103,8 +103,9 @@ static inline int get_ur_golomb_jpegls(GetBitContext *gb, int k, int limit, * read signed golomb rice code (flac). */ static inline int get_sr_golomb_flac(GetBitContext *gb, int k, int limit, int esc_len){ - int v= get_ur_golomb_jpegls(gb, k, limit, esc_len); - return (v>>1) ^ -(v&1); + /* The folded value can use all 32 bits, so unfold it as unsigned. */ + unsigned int v= get_ur_golomb_jpegls(gb, k, limit, esc_len); + return (int)((v>>1) ^ -(v&1)); } /** From 79329c8f7cc8ae3092c6b1a89f43d7f66a12a468 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 24 Sep 2026 00:30:51 +0200 Subject: [PATCH 282/408] rk27xx: don't return from SD transfers with the lock held sd_read_sectors() and sd_write_sectors() take sd_mtx and power the controller, then check the requested range and return -1 on failure - leaving the mutex held and the controller on. Check the range first. With no card present numblocks is 0, so every request takes that path. Rockbox mutexes are recursive for the owning thread, so the first thread to touch the SD drive keeps working, and every other thread that does blocks forever. Co-Authored-By: Claude Opus 5.5 Change-Id: I16c2be3bde5c62946fdedaabe115b6c4bc330467 --- firmware/target/arm/rk27xx/sd-rk27xx.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/firmware/target/arm/rk27xx/sd-rk27xx.c b/firmware/target/arm/rk27xx/sd-rk27xx.c index a49e8e5c43..02180d9faa 100644 --- a/firmware/target/arm/rk27xx/sd-rk27xx.c +++ b/firmware/target/arm/rk27xx/sd-rk27xx.c @@ -464,12 +464,15 @@ int sd_read_sectors(IF_MD(int drive,) sector_t start, int count, int cnt, ret = 0; unsigned char *dst; - mutex_lock(&sd_mtx); - enable_controller(true); - + /* Checked before taking the lock: returning from after it left sd_mtx + * held and the controller powered. With no card numblocks is 0, so + * that was every request. */ if (count <= 0 || start + count > card_info.numblocks) return -1; + mutex_lock(&sd_mtx); + enable_controller(true); + if(!(card_info.ocr & (1<<30))) start <<= 9; /* not SDHC */ @@ -602,12 +605,15 @@ int sd_write_sectors(IF_MD(int drive,) sector_t start, int count, unsigned char *src; /* bool card_selected = false; */ - mutex_lock(&sd_mtx); - enable_controller(true); - + /* Checked before taking the lock: returning from after it left sd_mtx + * held and the controller powered. With no card numblocks is 0, so + * that was every request. */ if (count <= 0 || start + count > card_info.numblocks) return -1; + mutex_lock(&sd_mtx); + enable_controller(true); + if(!(card_info.ocr & (1<<30))) start <<= 9; /* not SDHC */ From 475b4629b655527e28816bff676fde2f85892102 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 24 Sep 2026 14:46:41 +0200 Subject: [PATCH 283/408] rk27xx: attach to the bus only once the USB core is set up The UDC was only ever connected from the interrupt handler, on CONN_INTR. That needs a cable-insert edge after the stack is up, and there is none when the cable is already in - booting with it plugged, or taking the controller over from the ROM loader or hwstub, which leave it enumerated as a different device. The device then never enumerated. Connecting from usb_drv_init() would not do either: usb_core_init() calls it first, before the class drivers are set up and before the core sets its own state, so a fast host enumerated against state that was then overwritten and the descriptor read timed out - depending on timing. Add usb_drv_connect(), which drops off the bus, resets the PHY and reconnects, and call it from usb_enable() after usb_core_init() returns. Tested on a generic rk2705, loaded over hwstub with the cable in: the device drops off, comes back and enumerates as Rockbox. Co-Authored-By: Claude Opus 5.5 Change-Id: I372bd6a49594c00909ada0cc43b046178140e755 --- firmware/target/arm/rk27xx/usb-drv-rk27xx.c | 22 +++++++++++++++++++++ firmware/target/arm/rk27xx/usb-rk27xx.c | 6 ++++++ 2 files changed, 28 insertions(+) diff --git a/firmware/target/arm/rk27xx/usb-drv-rk27xx.c b/firmware/target/arm/rk27xx/usb-drv-rk27xx.c index fb1775f993..1b808df422 100644 --- a/firmware/target/arm/rk27xx/usb-drv-rk27xx.c +++ b/firmware/target/arm/rk27xx/usb-drv-rk27xx.c @@ -468,6 +468,28 @@ void usb_drv_init(void) semaphore_init(&endpoints[ep_num].complete, 1, 0); } +/* Present ourselves to the host. Called by usb_enable() once usb_core_init() + * has finished - not from usb_drv_init(), which usb_core_init() calls FIRST, + * before the class drivers are set up and before it sets its own state. A + * host quick enough to enumerate in that window had its requests handled + * against state usb_core_init() then overwrote: the descriptor read timed + * out, and whether it did depended on timing. + * + * Connecting only on CONN_INTR, as the interrupt handler does, needs a + * cable-insert edge after the stack is up - and there is none when the cable + * was already in: booting with it plugged, or taking the controller over + * from the ROM loader or hwstub, which leaves it enumerated as a different + * device the host has no reason to re-enumerate. So drop off the bus, reset + * the PHY and reconnect: the host sees a fresh device either way. */ +void usb_drv_connect(void) +{ + DEV_CTL &= ~DEV_SOFT_CN; + udelay(20000); + udc_phy_reset(); + udelay(10000); + udc_soft_connect(); +} + /* turn off usb core */ void usb_drv_exit(void) { diff --git a/firmware/target/arm/rk27xx/usb-rk27xx.c b/firmware/target/arm/rk27xx/usb-rk27xx.c index 4ac4f81c32..69f5bb195b 100644 --- a/firmware/target/arm/rk27xx/usb-rk27xx.c +++ b/firmware/target/arm/rk27xx/usb-rk27xx.c @@ -54,10 +54,16 @@ void usb_attach(void) usb_enable(true); } +void usb_drv_connect(void); /* usb-drv-rk27xx.c */ + void usb_enable(bool on) { if(on) + { usb_core_init(); + /* attach only once the core is fully set up - see usb_drv_connect() */ + usb_drv_connect(); + } else usb_core_exit(); } From 70c546113d97e76a20d666225825ed1185be27e2 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 23:20:13 +0200 Subject: [PATCH 284/408] rk27xx: bring the hardware-tested UDC fixes to the current USB core The rk27xx UDC driver was fixed and stress-tested on a generic rk2705 against the USB core of early 2026 - including a port to the control request API of that time. Meanwhile the core took over the EP0 state machine (usb_core_setup_received()) and endpoint allocation, and the driver was converted to both without the fixes. This carries them over. Transfers and resets (a mass-storage stress test fell off the bus after 10,000 - 27,000 operations without these): - Transfers are set up with interrupts disabled: the interrupt handler advances buf/cnt of the same endpoint for the next packet. - ep_write()'s wait for TXFULL to clear is bounded by an iteration count. It also runs in the interrupt handler, where current_tick never advances, so a tick timeout spun forever. - A bus reset cancels transfers - usb_drv_cancel_all_transfers() was empty - instead of re-initialising the completion semaphores, which loses a thread blocked on one for good; blocked senders are woken with an error and the enabled endpoints NAKed and flushed. The reset handler also calls usb_core_bus_reset(), which it never did. - Blocking sends time out after a second and report the error. - An ACK with no transfer armed (one cancelled by a reset) is ignored. Configuration: - The configuration number is DEV_INFO [11:8]; it was read as bits 10:7, bit 7 being DEV_EN, so configuration 1 was reported as 2. - The UDC completes SET_ADDRESS and SET_CONFIGURATION itself, raising no interrupt, so udc_helper() - which reports them from DEV_INFO - also runs from a tick task while the device is unconfigured. After a bus reset of a configured device the host re-sends SET_CONFIGURATION and goes straight to a bulk command that NAKs without interrupting: without the tick, the device never came back. EP0, with the core now running the control state machine: - Right after connect the UDC reports one SETUP with both registers zero; no host sends that, and it is ignored. - A SETUP clears a stall, and ends - reported to the core as failed - any EP0 transfer still in flight, which belongs to a request the host abandoned; otherwise the core would wait for it forever. EP0 stall uses the EP0 registers, not endpoints[0], a stub without registers. - Control reads are clipped to wLength and end with a zero length packet when a short answer fills whole packets. - The core arms status stages with no buffer; they land in a dummy one. - A status OUT arriving while the data IN is still going - the host took less than was offered - ends the data stage too. - At a bus reset EP0 transfers are dropped silently: the core resets its own EP0 state. Tested on a generic rk2705. The transfer, reset and configuration fixes first against the USB core of early 2026: RAM-disk, NAND and SD stress tests over USB mass storage, and usbreset recovery. Then the driver as it is here, on the current core: enumeration, and a mass-storage stress test of three LUNs at once (RAM disk, NAND, SD) that also passes after an eject and a cold power cycle. Co-Authored-By: Claude Opus 5.5 Change-Id: I1b2d03ca8f688f2a0e7b28ead64b7ab050a0a9e8 --- firmware/target/arm/rk27xx/usb-drv-rk27xx.c | 367 +++++++++++++++----- 1 file changed, 289 insertions(+), 78 deletions(-) diff --git a/firmware/target/arm/rk27xx/usb-drv-rk27xx.c b/firmware/target/arm/rk27xx/usb-drv-rk27xx.c index 1b808df422..33a1650fea 100644 --- a/firmware/target/arm/rk27xx/usb-drv-rk27xx.c +++ b/firmware/target/arm/rk27xx/usb-drv-rk27xx.c @@ -54,10 +54,15 @@ struct endpoint_t const int type; /* EP type */ const int dir; /* DIR_IN/DIR_OUT */ volatile unsigned long *stat; /* RXSTAT/TXSTAT register */ + bool enabled; /* set up by usb_drv_ep_init() */ volatile void *buf; /* tx/rx buffer address */ volatile int len; /* size of the transfer (bytes) */ volatile int cnt; /* number of bytes transfered/received */ volatile bool block; /* flag indicating that transfer is blocking */ + volatile bool busy; /* a transfer is armed and not yet finished */ + volatile bool zlp; /* IN: end a max-packet-sized transfer with a + * zero length packet */ + volatile int result; /* 0 = completed, -1 = cancelled */ struct semaphore complete; /* semaphore for blocking transfers */ }; @@ -74,7 +79,8 @@ struct endpoint_t #define DMAINLMADDR(endp) *(4 + (endp)->stat) #define ENDPOINT(num, type, dir, reg) \ - {num, USB_ENDPOINT_XFER_##type, USB_DIR_##dir, reg, NULL, 0, 0, true, {{0, 0}, 0, 0}} + {num, USB_ENDPOINT_XFER_##type, USB_DIR_##dir, reg, false, NULL, 0, 0, \ + true, false, false, 0, {{0, 0}, 0, 0}} static struct endpoint_t ctrlep[2] = { @@ -107,29 +113,77 @@ static volatile bool set_configuration = false; #undef ENDPOINT -static bool address_status; -static bool handle_set_address(struct usb_ctrlrequest *req); +/* EP0 control transfers: the USB core runs the control request state + * machine. The driver hands it each SETUP and reports the end of every EP0 + * transfer the core arms, the status stages included. + * + * The UDC completes SET_ADDRESS and SET_CONFIGURATION by itself: no SETUP + * reaches the driver for either, and udc_helper() reports them to the core + * from DEV_INFO instead. */ -/* SET_ADDRESS status belongs to this driver, not the core EP0 machine. */ -static void complete_transfer(int ep, int dir, int status, int length) +/* wLength of the latest request: an answer is never longer, and one shorter + * that fills whole packets ends with a zero length packet */ +static uint16_t ep0_wlength; + +/* status stage OUT lands here; the host sends no data, but DMA wants a + * target */ +static uint8_t ep0_dummy[64] __attribute__((aligned(32))); + +/* drop whatever EP0 transfer is in flight, both directions, silently */ +static void ep0_abort(void) { - if(ep == EP_CONTROL && address_status) - return; - usb_core_transfer_complete(ep, dir, status, length); + ctrlep[DIR_IN].busy = false; + ctrlep[DIR_OUT].busy = false; + + TX0CON |= TXNAK | TXCLR; + TX0CON &= ~TXCLR; + RX0CON |= RXNAK | RXCLR; + RX0CON &= ~RXCLR; } -static void setup_received(void) +/* End the EP0 transfers the core is waiting on as failed, so that its state + * machine gets back to taking requests. OUT first: in a control read the + * core expects the status OUT to end before the data IN. */ +static void ep0_fail_transfers(void) { - static uint32_t setup_data[2]; - logf("udc: setup"); + bool out = ctrlep[DIR_OUT].busy; + bool in = ctrlep[DIR_IN].busy; - /* copy setup data from packet */ - setup_data[0] = SETUP1; - setup_data[1] = SETUP2; + ep0_abort(); + if(out) + usb_core_transfer_complete(EP_CONTROL, USB_DIR_OUT, -1, 0); + if(in) + usb_core_transfer_complete(EP_CONTROL, USB_DIR_IN, -1, 0); +} - /* pass setup data to the upper layer */ - if(!handle_set_address((struct usb_ctrlrequest*)setup_data)) - usb_core_setup_received((struct usb_ctrlrequest*)setup_data); +static void ep0_setup_received(void) +{ + /* the two SETUP registers are the request's 8 bytes */ + union + { + uint32_t words[2]; + struct usb_ctrlrequest req; + } setup; + + setup.words[0] = SETUP1; + setup.words[1] = SETUP2; + logf("udc: setup %08lx %08lx", setup.words[0], setup.words[1]); + + /* Right after connect the UDC reports one SETUP with both registers + * zero. No host sends that - a host-to-device GET_STATUS - and answering + * it puts two bytes nobody asked for into the EP0 FIFO. */ + if(setup.words[0] == 0 && setup.words[1] == 0) + return; + + /* a SETUP ends a protocol stall */ + TX0CON &= ~TXSTALL; + RX0CON &= ~RXSTALL; + + /* a transfer still in flight belongs to a request the host abandoned */ + ep0_fail_transfers(); + + ep0_wlength = setup.req.wLength; + usb_core_setup_received(&setup.req); } static int max_pkt_size(struct endpoint_t *endp) @@ -146,11 +200,13 @@ static int max_pkt_size(struct endpoint_t *endp) static void ep_write(struct endpoint_t *endp) { int xfer_size = MIN(max_pkt_size(endp), endp->cnt); - unsigned int timeout = current_tick + HZ/10; - while(TXBUF(endp) & TXFULL) /* TXFULL flag */ + /* Bounded by iterations, not by current_tick: this also runs in the + * interrupt handler (the next packet of a transfer), where the tick + * never advances and a tick timeout spins forever. */ + for(int spin = 0; TXBUF(endp) & TXFULL; spin++) /* TXFULL flag */ { - if(TIME_AFTER(current_tick, timeout)) + if(spin > 200000) break; } @@ -193,10 +249,16 @@ static void in_intr(struct endpoint_t *endp) if(txstat & TXACK) { logf("udc: ack(%d)", endp->ep_num); - /* finished ? */ - if(endp->cnt <= 0) + /* nothing armed: a transfer cancelled by a bus reset */ + if(!endp->busy) + return; + /* finished ? cnt 0 means the last packet was a full one: done, + * unless the transfer must end with a zero length packet */ + if(endp->cnt < 0 || (endp->cnt == 0 && !endp->zlp)) { - complete_transfer(endp->ep_num, endp->dir, 0, endp->len); + endp->busy = false; + endp->result = 0; + usb_core_transfer_complete(endp->ep_num, endp->dir, 0, endp->len); /* release semaphore for blocking transfer */ if(endp->block) semaphore_release(&endp->complete); @@ -219,13 +281,34 @@ static void out_intr(struct endpoint_t *endp) /* check if a transfer has finished */ if(rxstat & RXACK) { + /* nothing armed: a transfer cancelled by a bus reset */ + if(!endp->busy) + return; int xfer_size = rxstat & 0xffff; endp->cnt -= xfer_size; endp->buf += xfer_size; logf("udc: ack(%d) -> %d/%d", endp->ep_num, xfer_size, endp->cnt); /* finished ? */ if(endp->cnt <= 0 || xfer_size < max_pkt_size(endp)) - complete_transfer(endp->ep_num, endp->dir, 0, endp->len); + { + endp->busy = false; + endp->result = 0; + usb_core_transfer_complete(endp->ep_num, endp->dir, 0, endp->len); + if(endp->block) + semaphore_release(&endp->complete); + /* EP0: a status OUT while the data IN is still going means the + * host took less than was offered, which is allowed - the data + * stage is over too */ + if(endp->ep_num == 0 && ctrlep[DIR_IN].busy) + { + struct endpoint_t *in = &ctrlep[DIR_IN]; + + in->busy = false; + TX0CON |= TXNAK | TXCLR; + TX0CON &= ~TXCLR; + usb_core_transfer_complete(EP_CONTROL, USB_DIR_IN, 0, in->len); + } + } else ep_read(endp); } @@ -263,15 +346,37 @@ static void udc_helper(void) usb_core_notify_set_address(dev_info & 0x7f); } - /* SET CONFIGURATION request */ + /* SET CONFIGURATION request - cfg_number is DEV_INFO [11:8], counted + * from 0; bit 7 is DEV_EN itself */ if(!set_configuration) if(dev_info & DEV_EN) { set_configuration = true; - usb_core_notify_set_config(((dev_info >> 7) & 0xf) + 1); + usb_core_notify_set_config(((dev_info >> 8) & 0xf) + 1); } } +/* The UDC completes SET_ADDRESS and SET_CONFIGURATION without raising an + * interrupt, so udc_helper() in the interrupt handler only notices them at + * the next UDC interrupt. At first enumeration GET_MAX_LUN provides one. + * After a bus reset of a configured device (usbreset, or an error recovery) + * the host re-sends SET_CONFIGURATION and goes straight to a bulk command: + * nothing else arrives on EP0, the bulk endpoint NAKs because the class was + * never re-armed, and NAKs raise no interrupt - the device hangs until the + * host times out, resets, and hangs again. So poll from the tick as well, + * for as long as the device is not configured. */ +static void udc_tick(void) +{ + if(!set_configuration) + udc_helper(); +} + +/* return port speed FS=0, HS=1 */ +int usb_drv_port_speed(void) +{ + return (DEV_INFO & DEV_SPEED) ? 0 : 1; +} + /* The UDC's endpoints come in groups of three - bulk OUT, bulk IN, * interrupt IN: 1-3, 4-6 and so on - each endpoint with one fixed type and * direction. An interrupt endpoint in the same group as bulk endpoints in @@ -319,37 +424,47 @@ void usb_drv_startup(void) { } -/* return port speed FS=0, HS=1 */ -int usb_drv_port_speed(void) -{ - return (DEV_INFO & DEV_SPEED) ? 0 : 1; -} - void usb_drv_ep_init(const struct usb_drv_ep_alloc_ctx* ctx, int ep) { /* FIXME: support max packet size override */ (void)ctx; - const int epnum = EP_NUM(ep); - const int epdir = EP_DIR(ep); + int ep_num = EP_NUM(ep); + struct endpoint_t *endp = &endpoints[ep_num]; - struct endpoint_t *endp = &endpoints[epnum]; - - if(epdir == DIR_IN) - TXCON(endp) = (epnum << 8) | TXEPEN | TXNAK | TXACKINTEN | TXCFINTE; + if(EP_DIR(ep) == DIR_IN) + TXCON(endp) = (ep_num << 8) | TXEPEN | TXNAK | TXACKINTEN | TXCFINTE; else - RXCON(endp) = (epnum << 8) | RXEPEN | RXNAK | RXACKINTEN | RXCFINTE | RXERRINTEN; - EN_INT |= 1 << (epnum + 7); + RXCON(endp) = (ep_num << 8) | RXEPEN | RXNAK | RXACKINTEN | RXCFINTE | + RXERRINTEN; + endp->enabled = true; + EN_INT |= 1 << (ep_num + 7); + logf("add: ep%d %s", ep_num, XFER_DIR_STR(EP_DIR(ep) == DIR_IN)); } void usb_drv_ep_deinit(const struct usb_drv_ep_alloc_ctx* ctx, int ep) { (void)ctx; - int num = EP_NUM(ep); + int ep_num = EP_NUM(ep); + + logf("rel: ep%d", ep_num); + endpoints[ep_num].enabled = false; /* disable interrupt from this endpoint */ - EN_INT &= ~(1 << (num + 7)); + EN_INT &= ~(1 << (ep_num + 7)); +} + +/* Set the address (usually it's in a register). + * There is a problem here: some controller want the address to be set between + * control out and ack and some want to wait for the end of the transaction. + * In the first case, you need to write some code special code when getting + * setup packets and ignore this function (have a look at other drives) + */ +void usb_drv_set_address(int address) +{ + (void)address; + /* UDC seems to set this automaticaly */ } static int _usb_drv_send(int endpoint, void *ptr, int length, bool block) @@ -358,24 +473,57 @@ static int _usb_drv_send(int endpoint, void *ptr, int length, bool block) struct endpoint_t *ep; int ep_num = EP_NUM(endpoint); + bool zlp = false; + if (ep_num == 0) + { ep = &ctrlep[DIR_IN]; + /* never send more than asked for, and end a short answer that fills + * whole packets with a zero length packet */ + if(length > ep0_wlength) + length = ep0_wlength; + zlp = length > 0 && length < ep0_wlength && length % 64 == 0; + } else ep = &endpoints[ep_num]; /* for send transfers, make sure the data is committed */ - commit_discard_dcache_range(ptr, length); + if(length) + commit_discard_dcache_range(ptr, length); + + /* drop a release left over from a transfer that ended (or was + * cancelled) after its waiter had already given up */ + if(block) + semaphore_wait(&ep->complete, TIMEOUT_NOBLOCK); + + /* the interrupt handler works on these fields too */ + int oldlevel = disable_irq_save(); ep->buf = ptr; ep->len = ep->cnt = length; ep->block = block; - + ep->zlp = zlp; + ep->busy = true; + ep->result = 0; ep_write(ep); + restore_irq(oldlevel); - /* wait for transfer to end */ - if(block) - semaphore_wait(&ep->complete, TIMEOUT_BLOCK); + if(!block) + return 0; - return 0; + /* wait for transfer to end - but not forever: a host that gave up on + * the transfer would otherwise wedge the calling thread (the USB + * thread, for control responses) for good */ + if(semaphore_wait(&ep->complete, HZ) == OBJ_WAIT_TIMEDOUT) + { + oldlevel = disable_irq_save(); + bool pending = ep->busy; + ep->busy = false; + restore_irq(oldlevel); + if(pending) + return -1; + } + + return ep->result; } /* Setup a send transfer. (blocking) */ @@ -398,24 +546,74 @@ int usb_drv_recv_nonblocking(int endpoint, void* ptr, int length) int ep_num = EP_NUM(endpoint); if(ep_num == 0) + { ep = &ctrlep[DIR_OUT]; + if(ptr == NULL) + ptr = ep0_dummy; + } else ep = &endpoints[ep_num]; /* for recv, discard the cache lines related to the buffer */ - commit_discard_dcache_range(ptr, length); + if(length) + commit_discard_dcache_range(ptr, length); + + /* the interrupt handler works on these fields too */ + int oldlevel = disable_irq_save(); ep->buf = ptr; ep->len = ep->cnt = length; + ep->block = false; + ep->busy = true; + ep->result = 0; ep_read(ep); + restore_irq(oldlevel); return 0; } -/* Kill all transfers. Usually you need to set a bit for each endpoint - * and flush fifos. You should also call the completion handler with - * error status for everything - */ +static void cancel_transfer(struct endpoint_t *endp) +{ + if(!endp->busy) + return; + + endp->busy = false; + endp->result = -1; + endp->cnt = 0; + /* wake a blocked sender - it sees result -1 */ + if(endp->block) + semaphore_release(&endp->complete); +} + +/* Kill all transfers: after a bus reset nothing armed before it will ever + * complete. Blocked senders are woken with an error. Class drivers are not + * called - the host re-configures after a reset, and init_connection() + * re-arms them from scratch. EP0 is not touched: a bus reset drops its + * transfers with ep0_abort(), and the core resets its own EP0 state. */ void usb_drv_cancel_all_transfers(void) { + int oldlevel = disable_irq_save(); + + for(int ep_num = 1; ep_num < USB_NUM_ENDPOINTS; ep_num++) + { + struct endpoint_t *endp = &endpoints[ep_num]; + + cancel_transfer(endp); + if(!endp->enabled) + continue; + + /* NAK and flush whatever is left in the FIFO */ + if(endp->dir == USB_DIR_IN) + { + TXCON(endp) |= TXNAK | TXCLR; + TXCON(endp) &= ~TXCLR; + } + else + { + RXCON(endp) |= RXNAK | RXCLR; + RXCON(endp) &= ~RXCLR; + } + } + + restore_irq(oldlevel); } /* Set test mode, you can forget that for now, usually it's sufficient @@ -426,10 +624,20 @@ void usb_drv_set_test_mode(int mode) (void)mode; } +/* endpoints[0] is a stub without registers: EP0 lives in ctrlep[] */ +static struct endpoint_t *stall_ep(int endpoint, bool in) +{ + int ep_num = EP_NUM(endpoint); + + if(ep_num == 0) + return &ctrlep[in ? DIR_IN : DIR_OUT]; + return &endpoints[ep_num]; +} + /* Check if endpoint is in stall state */ bool usb_drv_stalled(int endpoint, bool in) { - struct endpoint_t *endp = &endpoints[EP_NUM(endpoint)]; + struct endpoint_t *endp = stall_ep(endpoint, in); if(in) return !!(TXCON(endp) & TXSTALL); @@ -440,7 +648,7 @@ bool usb_drv_stalled(int endpoint, bool in) /* Stall the endpoint. Usually set a flag in the controller */ void usb_drv_stall(int endpoint, bool stall, bool in) { - struct endpoint_t *endp = &endpoints[EP_NUM(endpoint)]; + struct endpoint_t *endp = stall_ep(endpoint, in); if(in) { if(stall) @@ -457,15 +665,28 @@ void usb_drv_stall(int endpoint, bool stall, bool in) } } -/* one time init (once per connection) - basicaly enable usb core */ -void usb_drv_init(void) +/* Transfer-completion semaphores start fresh at every stack init. Never from + * the interrupt handler: re-initialising a semaphore a thread is blocked on + * loses that thread for good - a bus reset cancels transfers instead. */ +static void udc_reset_semaphores(void) { /* init semaphore of ep0 */ semaphore_init(&ctrlep[DIR_OUT].complete, 1, 0); semaphore_init(&ctrlep[DIR_IN].complete, 1, 0); + ctrlep[DIR_OUT].busy = ctrlep[DIR_IN].busy = false; for(int ep_num = 1; ep_num < USB_NUM_ENDPOINTS; ep_num++) + { semaphore_init(&endpoints[ep_num].complete, 1, 0); + endpoints[ep_num].busy = false; + } +} + +/* one time init (once per connection) - basicaly enable usb core */ +void usb_drv_init(void) +{ + udc_reset_semaphores(); + tick_add_task(udc_tick); } /* Present ourselves to the host. Called by usb_enable() once usb_core_init() @@ -493,6 +714,7 @@ void usb_drv_connect(void) /* turn off usb core */ void usb_drv_exit(void) { + tick_remove_task(udc_tick); DEV_CTL = DEV_SELF_PWR; /* disable USB interrupts in interrupt controller */ @@ -543,14 +765,18 @@ void INT_UDC(void) set_address = false; set_configuration = false; + + /* before any SETUP in this same interrupt is handled: whatever + * was in flight is gone, including the control request */ + usb_drv_cancel_all_transfers(); + ep0_abort(); + usb_core_bus_reset(); } /* This needs to be processed AFTER usb reset */ udc_helper(); - if(intsrc & SETUP_INTR) /* setup interrupt */ - { - setup_received(); - } + /* EP0 completions before a SETUP: when both are pending, the + * completion belongs to the older request, which the SETUP ends */ if(intsrc & IN0_INTR) { /* EP0 IN done */ @@ -561,10 +787,9 @@ void INT_UDC(void) /* EP0 OUT done */ out_intr(&ctrlep[DIR_OUT]); } - if(intsrc & USBRST_INTR) + if(intsrc & SETUP_INTR) /* setup interrupt */ { - /* usb reset */ - usb_drv_init(); + ep0_setup_received(); } if(intsrc & RESUME_INTR) { @@ -597,17 +822,3 @@ void INT_UDC(void) out_intr(endp); } } - -static bool handle_set_address(struct usb_ctrlrequest *req) -{ - address_status = false; - if(!usb_drv_is_set_address(req)) - return false; - - const uint8_t address = req->wValue & 0x7f; - address_status = true; - usb_drv_cancel_all_transfers(); - usb_drv_send_nonblocking(EP_CONTROL, NULL, 0); - usb_core_notify_set_address(address); - return true; -} From 0e1952a4d1af65ae11144ec591876fe6c1ce88ee Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 30 Sep 2026 16:42:20 +0200 Subject: [PATCH 285/408] rk27xx: invalidate the cache with the cache controller off commit_discard_idcache() invalidated both cache ways while running from cached SDRAM. When the loop polling for the invalidate to finish starts on a cache line of its own, it is fetched through the ways being invalidated and the CPU takes a data abort, reported at the loop's branch. Whether it crashed thus depended on where the linker put the function. usb_storage calls it on every USB connect. On rk27generic a jpeg change that grew clip_jpeg_fd by 8 bytes moved the loop onto a new line, and the firmware crashed as the USB screen came up, with an empty backtrace. Turn the cache controller off around the invalidate, as crt0.S does at start-up. The cache is write-through, so no data is lost. Tested on a generic rk2705 with the function padded so the poll loop starts a new cache line: without this change it crashes at the first USB connect, with it the device enumerates as a mass storage device. The normally linked build works too. Co-Authored-By: Claude Opus 5.5 Change-Id: I319b811fdef30717999b60132000e70f410001fc --- firmware/target/arm/rk27xx/system-rk27xx.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/firmware/target/arm/rk27xx/system-rk27xx.c b/firmware/target/arm/rk27xx/system-rk27xx.c index cf80e09509..ff0aaf38a9 100644 --- a/firmware/target/arm/rk27xx/system-rk27xx.c +++ b/firmware/target/arm/rk27xx/system-rk27xx.c @@ -208,11 +208,21 @@ static void cache_invalidate_way(int way) void commit_discard_idcache(void) { int old_irq = disable_irq_save(); + unsigned long devid = DEVID; + + /* Invalidate with the cache off, as crt0 does. This code runs from + * cached SDRAM: invalidating the ways while fetching through them fails + * when the poll loop starts on a cache line of its own, depending on + * where the linker happened to put it. The cache is write-through, so + * nothing is lost by turning it off. */ + DEVID = devid & ~(1UL << 31); cache_invalidate_way(0); cache_invalidate_way(1); + DEVID = devid; + restore_irq(old_irq); } void commit_discard_dcache (void) __attribute__((alias("commit_discard_idcache"))); From e45936397ee3677c910c9a0c6473184e9755040c Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 29 Sep 2026 19:18:11 -0400 Subject: [PATCH 286/408] opus: filter the PLC excitation in place At a CELT to hybrid mode switch, opus_decode_frame() runs CELT's pitch PLC nested in the new frame, and celt_decode_lost() held a copy of up to 2 KB of excitation for celt_fir(). On stackOverflow.opus that overran the 9 KB codec stack on native targets such as the e200v2. Filtering in place from the last sample down needs no copy; output is bit-identical. Worst case over all 242 mode switches of stackOverflow.opus, measured under qemu: 9140 -> 7676 bytes below opus_decode(), against 8912 left for it on the e200v2. Co-Authored-By: Claude Opus 5.5 Change-Id: Iaa9f57182ad86ab76c81c55bea72aa517cc18230 --- lib/rbcodec/codecs/libopus/README.rockbox | 2 ++ .../codecs/libopus/celt/celt_decoder.c | 23 +++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/rbcodec/codecs/libopus/README.rockbox b/lib/rbcodec/codecs/libopus/README.rockbox index b63fae2251..5d27580e9f 100644 --- a/lib/rbcodec/codecs/libopus/README.rockbox +++ b/lib/rbcodec/codecs/libopus/README.rockbox @@ -17,6 +17,8 @@ Celt: OPUS_ARM_INLINE_EDSP to OPUS_ARM_ASM_ARMV5E_AND_LATER throughout celt and silk, since config.h defines exactly one of them per core * add #define ABS(a)(((a) < 0) ? - (a) :(a)) to mathops.h +* celt_decode_lost() in celt_decoder.c filters the PLC excitation in place + (celt_fir_inplace) instead of through a copy, to fit the codec stack Silk: * added fixed-phase interpolation paths for the 8, 12 and 16 kHz to 48 kHz diff --git a/lib/rbcodec/codecs/libopus/celt/celt_decoder.c b/lib/rbcodec/codecs/libopus/celt/celt_decoder.c index 244331d764..6e36ee4d1c 100644 --- a/lib/rbcodec/codecs/libopus/celt/celt_decoder.c +++ b/lib/rbcodec/codecs/libopus/celt/celt_decoder.c @@ -504,6 +504,19 @@ static int celt_plc_pitch_search(celt_sig *decode_mem[2], int C, int arch) return pitch_index; } +/* Same output as celt_fir(), but running in place reduces stack use. */ +static void celt_fir_inplace(opus_val16 *x, const opus_val16 *num, int N, int ord) +{ + int i, j; + for (i=N-1;i>=0;i--) + { + opus_val32 sum = SHL32(EXTEND32(x[i]), SIG_SHIFT); + for (j=0;jwindow; c=0; do { @@ -682,11 +693,9 @@ static void celt_decode_lost(CELTDecoder * OPUS_RESTRICT st, int N, int LM) /* Initialize the LPC history with the samples just before the start of the region for which we're computing the excitation. */ { - /* Compute the excitation for exc_length samples before the loss. We need the copy - because celt_fir() cannot filter in-place. */ - celt_fir(exc+MAX_PERIOD-exc_length, lpc+c*LPC_ORDER, - fir_tmp, exc_length, LPC_ORDER, st->arch); - OPUS_COPY(exc+MAX_PERIOD-exc_length, fir_tmp, exc_length); + /* Compute the excitation for exc_length samples before the loss. */ + celt_fir_inplace(exc+MAX_PERIOD-exc_length, lpc+c*LPC_ORDER, + exc_length, LPC_ORDER); } /* Check if the waveform is decaying, and if so how fast. From 53bc6abc9762748706b3d7ef077418065b803b05 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 23 Sep 2026 19:02:59 +0200 Subject: [PATCH 287/408] rk27xx: fill in the NANDC BCH register definitions BCHCTL bit 13 and the whole of BCHST were marked unknown. Both are needed by any driver that reads NAND through the controller's ECC engine: a read has to be able to report an uncorrectable sector, and MLC parts want a refresh once a sector needs enough correction. BCHCTL bit 13 ECC strength, clear = t=8, set = t=14 (both m=13, poly 0x25af). Established by decoding both modes against real media until the stored ECC bytes reproduced. BCHST bit 0 result valid bit 2 error - uncorrectable when set together with bit 0 bits 6:3 number of corrected bit errors Taken from the rk2705 NAND bootloader's ECC read loop: tst r0,#1 ; tst r0,#4 both set -> sector uncorrectable lsl r0,#25 ; lsr r0,#28 -> (BCHST >> 3) & 0xf, corrected bits cmp r0,#3 >= 3 triggers a block refresh The threshold agrees with MlcRefreshHook in the Samsung OF's flash.o, so two independent firmwares say the same thing. Co-Authored-By: Claude Opus 5 Change-Id: I1e356d5510827b46035f1d7d4a2cc69f4e83f43f --- firmware/export/rk27xx.h | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/firmware/export/rk27xx.h b/firmware/export/rk27xx.h index 8ab94add82..4579a8914e 100644 --- a/firmware/export/rk27xx.h +++ b/firmware/export/rk27xx.h @@ -919,14 +919,35 @@ #define FL_RST (1<<0) #define BCHCTL (*(volatile unsigned long *)(AHB0_NANDC + 0x0C)) -/* bit 13 is used but unknown */ +/* ECC strength select: clear = t=8, set = t=14. Both are m=13, poly 0x25af. + * The OF uses t=8 for the boot region and for Samsung media, t=14 for the + * data area on some devices. Established by decoding both modes against real + * media until the stored ECC bytes reproduced. */ +#define BCH_T14 (1<<13) /* bit 12 is used but unknown */ #define BCH_WR (1<<1) #define BCH_RST (1<<0) #define BCHST (*(volatile unsigned long *)(AHB0_NANDC + 0xD0)) -/* bit 2 ERR ?? */ -/* bit 0 ?? */ +/* Read once FLCTL reports FL_RDY for the sector: + * + * bit 0 result valid + * bit 2 error - UNCORRECTABLE when set together with bit 0 + * bits 6:3 number of corrected bit errors + * + * From the rk2705 NAND bootloader's ECC read loop: + * tst r0,#1 ; tst r0,#4 both set -> sector is uncorrectable + * lsl r0,#25 ; lsr r0,#28 -> (BCHST >> 3) & 0xf, corrected bits + * cmp r0,#3 >= 3 triggers a block refresh + * The threshold agrees with MlcRefreshHook in the Samsung OF's flash.o, i.e. + * two independent firmwares. */ +#define BCH_VALID (1<<0) +#define BCH_ERR (1<<2) +#define BCH_CORRECTED(st) (((st) >> 3) & 0xf) +#define BCH_UNCORRECTABLE(st) \ + (((st) & (BCH_VALID|BCH_ERR)) == (BCH_VALID|BCH_ERR)) +/* corrected-bit count at which the OF schedules a block refresh */ +#define BCH_REFRESH_THRESHOLD 3 #define FLASH_DATA(n) (*(volatile unsigned char *)(AHB0_NANDC + 0x200 + (n<<9))) #define FLASH_ADDR(n) (*(volatile unsigned char *)(AHB0_NANDC + 0x204 + (n<<9))) From c3356ac35f76fba1455b8b7918cbcde1cc89761e Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 23 Sep 2026 19:03:19 +0200 Subject: [PATCH 288/408] rk27xx: build the raw NAND layer nand-rk27xx.c held a complete transcription of the OF's NAND handling - chip detection, geometry derivation, chip select, ECC reads - inside "#if 0", written as documentation back when the FTL was still unknown. It has therefore never been compiled. Enabling it exposed three things nothing had ever caught: - flash_init() looks up ManufactureIDTbl[] and DeviceCode[], but the tables are named manufacture_id_tbl[] and device_code[] - mlc_refresh_row, flash_pend_cmd and flash_read_status_cmd are assigned but were never defined - memcpy() was used without including string.h struct flashspec_t moves to nand-target.h, with flash_spec[] and total_phy_sec declared there, because the FTL's flash primitives need the geometry flash_init() derives. The "_raw" fields describe one physical plane and the others the multi-plane view the FTL addresses; that distinction is load-bearing for the FTL's address mapping, so both are kept. flash_read_page() is renamed flash_read_page_raw(). It reads a whole page unbuffered and without ECC, and the name is needed for the FTL primitive that does the ECC read. Co-Authored-By: Claude Opus 5 Change-Id: I3eb2d6f41d72b3239a0f9493e49532c242c588fa --- firmware/target/arm/rk27xx/nand-rk27xx.c | 76 +++++++++++++----------- firmware/target/arm/rk27xx/nand-target.h | 41 +++++++++++++ 2 files changed, 82 insertions(+), 35 deletions(-) diff --git a/firmware/target/arm/rk27xx/nand-rk27xx.c b/firmware/target/arm/rk27xx/nand-rk27xx.c index 2cf605e72b..ca39d0404f 100644 --- a/firmware/target/arm/rk27xx/nand-rk27xx.c +++ b/firmware/target/arm/rk27xx/nand-rk27xx.c @@ -19,46 +19,28 @@ ****************************************************************************/ #include "config.h" + #include "system.h" + #include "string.h" #include "nand-target.h" + #include "rk27xx.h" -#if 0 -/* This is for documentation purpose as FTL has not been reverse engineered yet - * Raw nand handling functions based on OF disassembly and partially inspired - * by Rockchip patent +/* Raw nand handling functions based on OF disassembly and partially inspired + * by Rockchip patent. + * + * This was written as documentation while the FTL was still unknown and sat + * under `#if 0` for years. The Scheme A FTL is now reconstructed + * (ftl-scheme-a.c) and needs exactly what this provides: chip detection and + * the geometry derived from it, plus chip select. Enabled. */ -#define MAX_FLASH_NUM 4 +/* MAX_FLASH_NUM now in nand-target.h */ #define CMD_READ_STATUS 0x70 #define CMD_RESET 0xFF #define CMD_READ_ID 0x90 #define READ_PAGE_CMD 0x30 -/* this is the struct OF uses */ -struct flashspec_t -{ - uint8_t cache_prog; - uint8_t mul_plane; - uint8_t interleave; - uint8_t large; - uint8_t five; - uint8_t mlc; - uint8_t vendor; - uint8_t access_time; - uint8_t sec_per_page; - uint8_t sec_per_page_raw; - uint16_t sec_per_block; - uint16_t sec_per_block_raw; - uint16_t page_per_block; - uint16_t page_per_block_raw; - - uint32_t tot_logic_sec; - uint32_t total_phy_sec; - uint32_t total_bloks; - - uint32_t cmd; - uint32_t addr; - uint32_t data; -}; +/* struct flashspec_t now lives in nand-target.h - the Scheme A FTL's flash + * primitives (flash-rk27xx.c) need the geometry flash_init() derives here. */ /* holds nand chips characteristics */ struct flashspec_t flash_spec[MAX_FLASH_NUM]; @@ -111,6 +93,31 @@ const uint32_t device_info[] = 0x800000 /* 4G, large page */ }; +/* State the OF keeps across flash operations. + * + * These were referenced by flash_init() but never transcribed - the block was + * under `#if 0`, so nothing caught it. They are defined here with the values + * flash_init() assigns, which is all the current code needs: + * + * - mlc_refresh_row: row scheduled for refresh after a sector needed + * BCH_REFRESH_THRESHOLD corrected bits; 0xffffffff means "none pending". + * - flash_pend_cmd: the OF defers a program command so it can be merged with + * the next one (cache programming). Nothing issues one yet. + * - flash_read_status_cmd: READ STATUS opcode, which differs on some + * multi-plane parts. + */ +uint32_t mlc_refresh_row; + +struct flash_pend_cmd_t +{ + uint8_t valid; + uint32_t row; +}; + +struct flash_pend_cmd_t flash_pend_cmd; + +uint8_t flash_read_status_cmd; + static int flash_delay(int n) { volatile int cnt, i, j; @@ -196,7 +203,7 @@ void flash_init(void) for (j=0; j Date: Thu, 24 Sep 2026 00:30:51 +0200 Subject: [PATCH 289/408] rk27xx: restore the missing 0xd5 NAND device code flash_init() identifies a chip by looking its READ ID device code up in device_code[] and taking the capacity from device_info[] at the same index. device_code[] had seven entries and device_info[] eight: 0xd5 (16 Gbit) was dropped when the table was transcribed from the OF. Every code after the gap picked up the capacity one row up, so a 0xd7 (32 Gbit, 4 GiB) part was sized at 2 GiB. On a generic rk2705 that halved total_phy_sec to 4194304, and the FTL looked for its tables in the wrong place and read erased flash. With the entry restored it reports 8388608, matching the chip and the host-side dump of the same unit. The OF's own table, as it appears in its NAND bootloader: 76 79 f1 da dc d3 d5 d7 00 00 02 00 00 00 04 00 ... A compile-time check now fails the build if the two tables differ in length again. Also fixes two register addresses in the same loop that were missing a digit (0x180E204/0x180E208 for 0x180E8204/0x180E8208); they are stored for reference only and nothing reads them. Co-Authored-By: Claude Opus 5.5 Change-Id: Ia70c249eea2e44ea34aef52d8ae20860b2b5db9d --- firmware/target/arm/rk27xx/nand-rk27xx.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/firmware/target/arm/rk27xx/nand-rk27xx.c b/firmware/target/arm/rk27xx/nand-rk27xx.c index ca39d0404f..0d0f7d9b02 100644 --- a/firmware/target/arm/rk27xx/nand-rk27xx.c +++ b/firmware/target/arm/rk27xx/nand-rk27xx.c @@ -58,7 +58,10 @@ enum vendor_t { ST }; -/* taken from OF */ +/* taken from OF - one entry per device_info[] row. The OF's table is + * 76 79 f1 da dc d3 d5 d7; 0xd5 was once missing here, which shifted every + * later code onto the capacity one row up and sized a 4 GiB 0xd7 part as + * 2 GiB. */ const uint8_t device_code[] = { 0x76, 0x79, @@ -66,6 +69,7 @@ const uint8_t device_code[] = { 0xda, 0xdc, 0xd3, + 0xd5, 0xd7 }; @@ -93,6 +97,11 @@ const uint32_t device_info[] = 0x800000 /* 4G, large page */ }; +/* device_code[j] is looked up and device_info[j] used - a length mismatch + * silently mis-sizes every chip after the gap */ +extern char device_tables_match[(sizeof(device_code) == + sizeof(device_info) / sizeof(device_info[0])) ? 1 : -1]; + /* State the OF keeps across flash operations. * * These were referenced by flash_init() but never transcribed - the block was @@ -183,8 +192,8 @@ void flash_init(void) * just for reference what OF does */ flash_spec[i].cmd = 0x180E8200 + (i<<9); - flash_spec[i].addr = 0x180E204 + (i<<9); - flash_spec[i].data = 0x180E208 + (i<<9); + flash_spec[i].addr = 0x180E8204 + (i<<9); + flash_spec[i].data = 0x180E8208 + (i<<9); flash_chip_select(i); FLASH_CMD(i) = CMD_RESET; /* write cmd to flash chip */ From 455b1bcbf8349425d7971b5bf87e791135da06af Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 10:16:34 +0200 Subject: [PATCH 290/408] rk27xx: add the NAND flash layer flash-rk27xx.c drives the NAND controller and its BCH engine for the flash translation layer that follows. It knows the chip's geometry and how to read, program, erase and copy sectors, but not what the sectors mean. - Addresses are 512-byte sectors in a linear view of the chip where a block is a super-block: on a two-plane part, one physical block from each plane, consecutive pages alternating between them. The layer maps that view to the chip; the FTL never sees planes. - Every sector carries 16 spare bytes: 13 of BCH code, generated and checked by the hardware, and 3 for the FTL. Byte 1 is written 0x00 on every program - the "page programmed" marker the original firmware's FTL keys its mount and recovery on. - The program sequence was read out of the original firmware's own machine code. The write kick is the read kick plus FL_WR, and the BCH engine needs BCH_WR to encode rather than decode. - A copy is read through the ECC engine and programmed, never the chip's internal data move, which on this MLC part would carry bit errors forward. - Writes are refused until the FTL enables them, and writes into the boot area are dropped and reported successful, as the original firmware does. The write-protect line is lifted only for the duration of each program or erase. - Failures and timeouts are counted: the FTL can act on few of them. Only the first chip is handled; every device the FTL has been checked on has one. Tested on a generic rk2705 directly, on a free block: a program across both planes, a single-sector program with metadata, a whole page, a copy and an erase each read back byte-exact through the controller's ECC decode, which also shows the code it generated is valid. Co-Authored-By: Claude Opus 5.5 Change-Id: If6ffa9f9811172f4f8829c1e68d6de9466a022c8 --- firmware/SOURCES | 1 + firmware/target/arm/rk27xx/flash-rk27xx.c | 617 ++++++++++++++++++++++ firmware/target/arm/rk27xx/flash-rk27xx.h | 118 +++++ 3 files changed, 736 insertions(+) create mode 100644 firmware/target/arm/rk27xx/flash-rk27xx.c create mode 100644 firmware/target/arm/rk27xx/flash-rk27xx.h diff --git a/firmware/SOURCES b/firmware/SOURCES index 0664e5788a..81e4459bea 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1711,6 +1711,7 @@ target/arm/rk27xx/backlight-rk27xx.c target/arm/rk27xx/adc-rk27xx.c target/arm/rk27xx/sd-rk27xx.c target/arm/rk27xx/ftl-rk27xx.c +target/arm/rk27xx/flash-rk27xx.c target/arm/rk27xx/nand-rk27xx.c target/arm/rk27xx/usb-rk27xx.c target/arm/rk27xx/lcdif-rk27xx.c diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.c b/firmware/target/arm/rk27xx/flash-rk27xx.c new file mode 100644 index 0000000000..c419efcc9f --- /dev/null +++ b/firmware/target/arm/rk27xx/flash-rk27xx.c @@ -0,0 +1,617 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* NAND access for the rk27xx flash translation layer - see flash-rk27xx.h. + * + * THE CONTROLLER + * + * Commands and addresses are written to the chip through FLASH_CMD/ADDR, one + * cycle per write. Data moves a sector at a time between the chip and one of + * four controller slots - 512 bytes of PAGE_BUF and 16 of SPARE_BUF each - + * with the BCH engine in the path: on a read it corrects the sector and + * reports the result in BCHST, on a program it computes the code into the + * last 13 spare bytes. A transfer is started with FLCTL; the slot is chosen + * by bits 3-4. + * + * A read latches a whole page in the chip, then streams its sectors out in + * order from the first: reaching sector k of a page means transferring the + * k before it too. + * + * A program sends the column, the sectors, then the confirm command, and + * reads the chip's status: one program operation per page between erases, + * as this MLC part allows no more (NOP = 1). + * + * The chip's write-protect line is lifted only for the duration of each + * program or erase. */ + +#include "config.h" +#include "system.h" +#include "string.h" +#include "rk27xx.h" +#include "nand-target.h" +#include "flash-rk27xx.h" + +#define CMD_READ_1ST 0x00 +#define CMD_READ_2ND 0x30 +#define CMD_PROG_1ST 0x80 +#define CMD_PROG_2ND 0x10 +#define CMD_ERASE_1ST 0x60 +#define CMD_ERASE_2ND 0xD0 +#define CMD_STATUS 0x70 +#define NAND_STATUS_FAIL 0x01 + +#define SPARE_SIZE 16 /* spare bytes per sector on the chip */ +#define SECTOR_STRIDE (FLASH_SECTOR_SIZE + SPARE_SIZE) + +/* Metadata byte 1 of every programmed sector: "programmed" */ +#define META_PROGRAMMED 1 + +/* Transfer one sector between the chip and a controller slot: ECC on, + * transfer on, bit 5 (region select), start. A write adds FL_WR, and the + * BCH engine then encodes (BCH_WR) rather than decodes. */ +#define FL_KICK_READ (FL_COR_EN | FL_XFER_EN | (1<<5) | FL_START) +#define FL_KICK_WRITE (FL_KICK_READ | FL_WR) + +/* Busy limits: the datasheet maxima (tPROG 2.2 ms, tBERS 10 ms) with margin */ +#define PROG_TIMEOUT_US 5000 +#define ERASE_TIMEOUT_US 20000 + +static struct flash_geometry geo; +static bool ready; +static bool writable; +static uint32_t boot_area = UINT32_MAX; +static struct flash_stats stats; + +int flash_layer_init(void) +{ + const struct flashspec_t *f = &flash_spec[0]; + int ret = 0; + + ready = false; + + if (f->total_phy_sec == 0 || f->sec_per_page_raw == 0 || + f->sec_per_block_raw == 0) + { + ret = 1; + } + /* the plane interleave below knows one plane or two */ + else if (f->mul_plane != 1 && f->mul_plane != 2) + { + ret = 2; + } + else if (f->sec_per_page_raw * f->mul_plane > FLASH_MAX_SEC_PER_PAGE) + { + ret = 3; + } + else + { + geo.planes = f->mul_plane; + geo.sec_per_page_raw = f->sec_per_page_raw; + geo.sec_per_page = f->sec_per_page; + geo.sec_per_block_raw = f->sec_per_block_raw; + geo.sec_per_block = f->sec_per_block; + geo.total_blocks = f->total_bloks; + geo.total_sectors = f->total_phy_sec; + ready = true; + } + return ret; +} + +const struct flash_geometry *flash_get_geometry(void) +{ + return &geo; +} + +void flash_set_writable(bool on) +{ + writable = on; +} + +void flash_set_boot_area(uint32_t sectors) +{ + boot_area = sectors; +} + +void flash_get_stats(struct flash_stats *out) +{ + *out = stats; +} + +/* Map an FTL sector to a raw sector on the chip. + * + * On a two-plane part a super-block is a pair of physical blocks, and + * consecutive pages alternate between them: + * + * super-block sec / (spb_raw * 2) + * within it page = L / (spp_raw * 2) + * plane = (L % (spp_raw * 2)) / spp_raw + * slot = L % spp_raw + * raw = (super_block * 2 + plane) * spb_raw + page * spp_raw + slot + * + * On a single-plane part it is the identity. */ +static uint32_t sec_to_raw(uint32_t sec) +{ + uint32_t raw = sec; + + if (geo.planes == 2) + { + uint32_t spb = (uint32_t)geo.sec_per_block_raw * 2; + uint32_t sb = sec / spb; + uint32_t l = sec % spb; + uint32_t page = l / ((uint32_t)geo.sec_per_page_raw * 2); + uint32_t plane = (l % ((uint32_t)geo.sec_per_page_raw * 2)) + / geo.sec_per_page_raw; + uint32_t slot = l % geo.sec_per_page_raw; + + raw = (sb * 2 + plane) * geo.sec_per_block_raw + + page * geo.sec_per_page_raw + slot; + } + return raw; +} + +static void wait_flash_ready(void) +{ + while (!(FMCTL & FM_RDY)) + { + } +} + +/* R/B# drops within tWB (100 ns) of a confirm command, so wait that long + * before polling, or a still-high line reads as done. */ +static bool wait_ready_us(uint32_t us) +{ + bool timeout = false; + + udelay(1); + while (!(FMCTL & FM_RDY) && !timeout) + { + if (us-- == 0) + { + stats.timeouts++; + timeout = true; + } + else + { + udelay(1); + } + } + return timeout; +} + +static void send_row(uint32_t row) +{ + FLASH_ADDR(0) = row & 0xff; + FLASH_ADDR(0) = (row >> 8) & 0xff; + FLASH_ADDR(0) = (row >> 16) & 0xff; +} + +/* Latch raw page `row` in the chip and prepare the ECC engine to decode. */ +static void latch_page(uint32_t row) +{ + flash_chip_select(0); + wait_flash_ready(); + + FLASH_CMD(0) = CMD_READ_1ST; + FLASH_ADDR(0) = 0x00; + FLASH_ADDR(0) = 0x00; + send_row(row); + FLASH_CMD(0) = CMD_READ_2ND; + + wait_flash_ready(); + + /* ECC on, t=8 - right for the whole chip on the devices seen */ + BCHCTL = BCH_RST; +} + +/* Transfer the next sector of the latched page through slot `slot` & 3. + * Returns the BCH status. */ +static uint32_t read_next_sector(uint32_t slot, uint8_t *data, uint8_t *meta) +{ + uint32_t st; + uint32_t buf = slot & 3; + + FLCTL = FL_KICK_READ | (buf << 3); + while (!(FLCTL & FL_RDY)) + { + } + + st = BCHST; + + if (data) + { + memcpy(data, (const void *)((uintptr_t)&PAGE_BUF + (buf << 9)), + FLASH_SECTOR_SIZE); + } + if (meta) + { + memcpy(meta, (const void *)((uintptr_t)&SPARE_BUF + (buf << 4)), + FLASH_META_SIZE); + } + return st; +} + +/* Read sectors [first, first + n) of raw page `row`. Returns 1 if any was + * uncorrectable. */ +static int read_raw_run(uint32_t row, uint32_t first, uint32_t n, + uint8_t *data, uint8_t *meta) +{ + uint32_t j; + int uncorrectable = 0; + + latch_page(row); + + for (j = 0; j < first + n; j++) + { + bool wanted = j >= first; + uint32_t k = j - first; + uint8_t *d = (wanted && data) ? data + (size_t)k * FLASH_SECTOR_SIZE + : NULL; + uint8_t *m = (wanted && meta) ? meta + (size_t)k * FLASH_META_SIZE + : NULL; + uint32_t st = read_next_sector(j, d, m); + + if (wanted) + { + if (BCH_UNCORRECTABLE(st)) + { + uncorrectable = 1; + } + else if (BCH_CORRECTED(st) >= BCH_REFRESH_THRESHOLD) + { + stats.refresh_pending++; + } + } + } + + flash_chip_deselect(); + return uncorrectable; +} + +int flash_read_raw(uint32_t raw_sec, void *data, void *meta) +{ + int ret = 1; + + if (ready && raw_sec < geo.total_sectors) + { + ret = read_raw_run(raw_sec / geo.sec_per_page_raw, + raw_sec % geo.sec_per_page_raw, 1, data, meta); + } + return ret; +} + +int flash_read(uint32_t sec, void *data, void *meta, unsigned n) +{ + uint8_t *d = data; + uint8_t *m = meta; + unsigned i; + int ret = ready ? 0 : 1; + + /* One page latch per sector. Reading whole runs in one pass is an + * obvious speed-up, left until the access pattern has been measured. */ + for (i = 0; i < n && ready; i++) + { + uint32_t raw = sec_to_raw(sec + i); + + if (raw >= geo.total_sectors) + { + ret = 1; + break; + } + + if (read_raw_run(raw / geo.sec_per_page_raw, + raw % geo.sec_per_page_raw, 1, + d ? d + (size_t)i * FLASH_SECTOR_SIZE : NULL, + m ? m + (size_t)i * FLASH_META_SIZE : NULL)) + { + ret = 1; + } + } + return ret; +} + +/* ---- writing ---- */ + +static bool write_refused(void) +{ + bool refused = !ready || !writable; + + if (refused) + { + stats.write_refusals++; + } + return refused; +} + +static uint8_t read_status(void) +{ + FLASH_CMD(0) = CMD_STATUS; + return FLASH_DATA(0); +} + +/* The page and spare buffers are written a word at a time; byte stores are + * not known to work. src may be unaligned, or NULL for erased content. */ +static void put_words(uintptr_t dst, const uint8_t *src, uint32_t len) +{ + volatile uint32_t *d = (volatile uint32_t *)dst; + uint32_t i; + + for (i = 0; i < len / 4; i++) + { + if (src == NULL) + { + d[i] = 0xffffffff; + } + else + { + d[i] = src[4*i] | (src[4*i + 1] << 8) | + (src[4*i + 2] << 16) | ((uint32_t)src[4*i + 3] << 24); + } + } +} + +/* Program `n` sectors of raw page `row`, from sector `first` in it. */ +static int prog_raw_run(uint32_t row, uint32_t first, uint32_t n, + const uint8_t *data, const uint8_t *meta) +{ + uint32_t col = first * SECTOR_STRIDE; + uint32_t i; + int fail; + + flash_chip_select(0); + FMCTL |= FM_PROTECT; /* lift WP# for this operation */ + wait_flash_ready(); + + FLASH_CMD(0) = CMD_PROG_1ST; + FLASH_ADDR(0) = col & 0xff; + FLASH_ADDR(0) = (col >> 8) & 0xff; + send_row(row); + + for (i = 0; i < n; i++) + { + uint32_t buf = i & 3; + uint8_t spare[SPARE_SIZE]; + + memset(spare, 0xff, sizeof(spare)); + if (meta) + { + memcpy(spare, meta + i * FLASH_META_SIZE, FLASH_META_SIZE); + } + spare[META_PROGRAMMED] = 0x00; + + /* a slot is reused every four sectors: its last transfer must be + * done */ + while (!(FLCTL & FL_RDY)) + { + } + + put_words((uintptr_t)&PAGE_BUF + (buf << 9), + data ? data + (size_t)i * FLASH_SECTOR_SIZE : NULL, + FLASH_SECTOR_SIZE); + put_words((uintptr_t)&SPARE_BUF + (buf << 4), spare, SPARE_SIZE); + + BCHCTL = BCH_WR | BCH_RST; + FLCTL = FL_KICK_WRITE | (buf << 3); + } + + while (!(FLCTL & FL_RDY)) + { + } + + FLASH_CMD(0) = CMD_PROG_2ND; + fail = wait_ready_us(PROG_TIMEOUT_US) || (read_status() & NAND_STATUS_FAIL); + + flash_chip_deselect(); + FMCTL &= ~FM_PROTECT; + + if (fail) + { + stats.prog_failures++; + } + return fail ? 1 : 0; +} + +static int erase_raw_block(uint32_t row) +{ + int fail; + + flash_chip_select(0); + FMCTL |= FM_PROTECT; + wait_flash_ready(); + + FLASH_CMD(0) = CMD_ERASE_1ST; + send_row(row); + FLASH_CMD(0) = CMD_ERASE_2ND; + + fail = wait_ready_us(ERASE_TIMEOUT_US) + || (read_status() & NAND_STATUS_FAIL); + + flash_chip_deselect(); + FMCTL &= ~FM_PROTECT; + + if (fail) + { + stats.erase_failures++; + } + return fail ? 1 : 0; +} + +/* Sectors of the FTL's view are split into one program per raw page - and + * so per plane. */ +int flash_program(uint32_t sec, const void *data, const void *meta, unsigned n) +{ + const uint8_t *d = data; + const uint8_t *m = meta; + uint32_t i = 0; + int ret = 0; + + if (n == 0) + { + ret = 0; + } + else if (write_refused()) + { + ret = 1; + } + else if (sec < boot_area) + { + stats.boot_area_skips++; + } + else + { + while (i < n && ret == 0) + { + uint32_t raw = sec_to_raw(sec + i); + uint32_t row = raw / geo.sec_per_page_raw; + uint32_t first = raw % geo.sec_per_page_raw; + uint32_t run = 1; + + if (raw >= geo.total_sectors) + { + ret = 1; + break; + } + + /* extend the run while the next sector is the next slot of the + * same raw page */ + while (i + run < n && first + run < geo.sec_per_page_raw && + sec_to_raw(sec + i + run) == raw + run) + { + run++; + } + + if (prog_raw_run(row, first, run, + d ? d + (size_t)i * FLASH_SECTOR_SIZE : NULL, + m ? m + (size_t)i * FLASH_META_SIZE : NULL)) + { + ret = 1; + } + i += run; + } + } + return ret; +} + +int flash_program_page(uint32_t sec, const void *data, const void *meta) +{ + return flash_program(sec & ~(uint32_t)(geo.sec_per_page - 1), data, meta, + geo.sec_per_page); +} + +int flash_erase(uint32_t sec) +{ + uint32_t base = sec - sec % geo.sec_per_block; + uint32_t p; + int ret = 0; + + if (write_refused()) + { + ret = 1; + } + else if (sec < boot_area) + { + stats.boot_area_skips++; + } + else if (sec_to_raw(base) >= geo.total_sectors) + { + ret = 1; + } + else + { + for (p = 0; p < geo.planes; p++) + { + uint32_t raw = sec_to_raw(base + p * geo.sec_per_page_raw); + + if (erase_raw_block(raw / geo.sec_per_page_raw)) + { + ret = 1; + } + } + } + return ret; +} + +/* Copy through the ECC engine, never with the chip's internal data move: on + * this MLC part moved pages accumulate bit errors, and the data area's + * t=8 is already below the chip's 12-bit minimum. A source sector that + * fails ECC is copied as read, and counted. */ +int flash_copy(uint32_t src, uint32_t dst, unsigned n) +{ + static uint8_t buf[FLASH_MAX_SEC_PER_PAGE * FLASH_SECTOR_SIZE] + __attribute__((aligned(4))); + uint32_t k = 0; + int ret = 0; + + if (n == 0 || n > geo.sec_per_block) + { + ret = 0; + } + else if (write_refused()) + { + ret = 1; + } + else + { + while (k < n && ret == 0) + { + /* one destination raw page at a time */ + uint32_t len = geo.sec_per_page_raw + - (dst + k) % geo.sec_per_page_raw; + uint32_t i = 0; + + if (len > n - k) + { + len = n - k; + } + + /* the source may straddle raw pages if its offset differs */ + while (i < len && ret == 0) + { + uint32_t raw = sec_to_raw(src + k + i); + uint32_t first = raw % geo.sec_per_page_raw; + uint32_t run = 1; + + if (raw >= geo.total_sectors) + { + ret = 1; + break; + } + while (i + run < len && first + run < geo.sec_per_page_raw && + sec_to_raw(src + k + i + run) == raw + run) + { + run++; + } + + if (read_raw_run(raw / geo.sec_per_page_raw, first, run, + buf + (size_t)i * FLASH_SECTOR_SIZE, NULL)) + { + stats.copy_uncorrectable++; + } + i += run; + } + + if (ret == 0 && flash_program(dst + k, buf, NULL, len)) + { + ret = 1; + } + k += len; + } + } + return ret; +} diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.h b/firmware/target/arm/rk27xx/flash-rk27xx.h new file mode 100644 index 0000000000..952d537ffc --- /dev/null +++ b/firmware/target/arm/rk27xx/flash-rk27xx.h @@ -0,0 +1,118 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* NAND access for the rk27xx flash translation layer. + * + * This layer drives the NAND controller and its BCH engine and nothing else: + * it knows the chip's geometry and how to read, program, erase and copy + * sectors, but not what the sectors mean. The FTL (ftl-scheme-a.c) is built + * on it. + * + * ADDRESSES are sector numbers (512 bytes) in the FTL's linear view of the + * chip, where a block is a SUPER-BLOCK: on a two-plane part, one physical + * block from each plane, with consecutive pages alternating between the + * planes. This layer maps that view to the chip; the FTL never sees planes. + * + * METADATA: every sector carries 16 spare bytes. The last 13 are the BCH + * code and belong to the hardware; the first three are the FTL's, and are + * what the meta arguments below carry - FLASH_META_SIZE bytes per sector. + * Byte 1 is special: this layer writes 0x00 there on every program, so that + * the FTL can tell a programmed page from an erased one whatever it wrote. + * + * Only the first chip is supported; every device the FTL has been validated + * on has one. */ + +#ifndef __FLASH_RK27XX_H__ +#define __FLASH_RK27XX_H__ + +#include +#include + +#define FLASH_SECTOR_SIZE 512 +#define FLASH_META_SIZE 3 /* FTL metadata bytes per sector */ + +/* The largest page the layer handles: 8 sectors per plane, two planes. */ +#define FLASH_MAX_SEC_PER_PAGE 16 + +struct flash_geometry +{ + uint8_t planes; /* 1 or 2 */ + uint8_t sec_per_page_raw; /* sectors in one physical page */ + uint8_t sec_per_page; /* sectors in one page of every plane */ + uint16_t sec_per_block_raw; /* sectors in one physical block */ + uint16_t sec_per_block; /* sectors in one super-block */ + uint32_t total_blocks; /* super-blocks on the chip */ + uint32_t total_sectors; +}; + +/* Write-path error counters. Most write results do not change what the FTL + * does next - it has no way to recover from most of them - so these are the + * record that something went wrong. */ +struct flash_stats +{ + uint32_t prog_failures; /* programs the chip reported failed */ + uint32_t erase_failures; + uint32_t timeouts; /* the chip never became ready */ + uint32_t boot_area_skips; /* writes into the boot area, dropped */ + uint32_t write_refusals; /* writes attempted while not writable */ + uint32_t copy_uncorrectable;/* copy sources failing ECC, copied anyway */ + uint32_t refresh_pending; /* reads near the ECC limit: blocks decaying */ +}; + +/* Take the geometry flash_init() detected. Returns 0 if the chip is usable. */ +int flash_layer_init(void); +const struct flash_geometry *flash_get_geometry(void); + +/* Read n sectors from sec. data (n * 512 bytes) and meta (n * 3 bytes) may + * each be NULL. Returns 0, or 1 if any sector was uncorrectable - its data + * is then delivered as read. */ +int flash_read(uint32_t sec, void *data, void *meta, unsigned n); + +/* Program n sectors from sec, which must be erased. data NULL programs + * 0xff; meta NULL programs {0xff, 0x00, 0xff}. Returns 0 or 1 on failure. */ +int flash_program(uint32_t sec, const void *data, const void *meta, unsigned n); + +/* Program the whole page (every plane) that contains sec. */ +int flash_program_page(uint32_t sec, const void *data, const void *meta); + +/* Erase the super-block containing sec. */ +int flash_erase(uint32_t sec); + +/* Copy n sectors (at most one super-block) from src to dst through the ECC + * engine, so that correctable errors are not propagated. The destination + * gets fresh metadata {0xff, 0x00, 0xff}. */ +int flash_copy(uint32_t src, uint32_t dst, unsigned n); + +/* Read one PHYSICAL sector, bypassing the super-block view: for the boot + * area, which the boot ROM addresses physically. */ +int flash_read_raw(uint32_t raw_sec, void *data, void *meta); + +/* Writes - program, erase and copy - are refused (return 1, counted) until + * the FTL has confirmed it understands the media and enables them. */ +void flash_set_writable(bool writable); + +/* The first `sectors` sectors hold the boot area. A write there is dropped + * and reported successful, as the original firmware does: no FTL error may + * reach the bootloader. UINT32_MAX protects the whole chip. */ +void flash_set_boot_area(uint32_t sectors); + +void flash_get_stats(struct flash_stats *stats); + +#endif /* __FLASH_RK27XX_H__ */ From f428c491d19c983e897adce5c900480543014421 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 11:21:09 +0200 Subject: [PATCH 291/408] rk27xx: keep the NAND controller clocked system_init() gates the clocks of modules a firmware build does not use, and the list included the NAND controller's HCLK. Once the NAND is storage, the first access to the controller - flash_init() writing FMWAIT at 0x180e8004 - takes a data abort on the unclocked peripheral. No rk27xx target stored to NAND before, which is why this never showed. The LCDC clock in the same list stays gated: the firmware draws with it gated, the MCU interface running from the LCDC HCLK, which is not. Co-Authored-By: Claude Opus 5.5 Change-Id: I52a4dc9e63fe5b89c85b60dc3ef77d74528dc06f --- firmware/target/arm/rk27xx/system-rk27xx.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firmware/target/arm/rk27xx/system-rk27xx.c b/firmware/target/arm/rk27xx/system-rk27xx.c index ff0aaf38a9..6cbc3aa9ae 100644 --- a/firmware/target/arm/rk27xx/system-rk27xx.c +++ b/firmware/target/arm/rk27xx/system-rk27xx.c @@ -129,7 +129,8 @@ void system_init(void) MCSDR_T_RP = 1; /* precharge period */ MCSDR_T_RCD = 1; /* active to RD/WR delay */ - /* turn off clock for unused modules */ + /* turn off clock for unused modules - not the NAND controller's: the + * FTL uses it, and a register access with its HCLK gated aborts */ SCU_CLKCFG |= CLKCFG_WDT | /* WDT pclk */ CLKCFG_RTC | /* RTC pclk */ CLKCFG_HSADC | /* HS_ADC clock */ @@ -140,7 +141,6 @@ void system_init(void) CLKCFG_VIP | /* VIP clock */ CLKCFG_HCLK_VIP | /* VIP HCLK */ CLKCFG_LCDC | /* LCDC clock */ - CLKCFG_NAND | /* NAND HCLK */ CLKCFG_UHC | /* USB host HCLK */ CLKCFG_DSP | /* DSP clock */ CLKCFG_OTP; /* OTP clock (dunno what it is */ From a0c1085f3f2f4db1efb696dc48ec8e3e245955ba Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 23 Sep 2026 19:03:43 +0200 Subject: [PATCH 292/408] rk27xx: add the Scheme A FTL ftl-rk27xx.c has been four empty stubs since 2010. Fill them in with the flash translation layer the rk2705/rk2706 original firmware uses, called Scheme A here to tell it from the log-structured layout of later firmware. It reads and writes that format exactly as the original firmware does, so a device keeps working with its original firmware after Rockbox has written to it. - ftl-scheme-a.{c,h}: the FTL. The file opens with a description of the on-flash format and how the FTL works: the SYS and USER volumes, super-blocks and zones, the zone table, the remap log and its mirror, the exchange record and the write protocol, power-loss recovery, bad blocks. Oddities of the original firmware kept for compatibility are marked where they are. - Parameters that differ between firmware builds - the zone reserve base, the system zone offset, the format flag - are recovered from the media at mount and checked against its structure; a mount that cannot confirm them is read-only. A mount that would have to repair the remap log while not allowed to write fails rather than serve wrong data. - ftl-rk27xx.c: the storage glue. It finds the boot area's ID block, which records where SYS ends, and mounts the FTL. - ata-nand-rk27xx.c: a drive per volume. SYS holds the original firmware - on a Rockbox device including the BASE.RKW that chainloads the bootloader - and nothing of the user's, so it is a drive only when the target defines HAVE_RK27XX_NAND_SYS. Capacity comes from the FTL's tables, not from raw block geometry. - config.h: HAVE_STORAGE_FLUSH for the rk27xx NAND. The FTL holds up to three part-written pages in RAM; storage_flush() commits them at shutdown and ROLO. Writing is opt-in: without FTL_ALLOW_WRITE the FTL mounts read-only and never writes the flash, not even a repair the mount could make. Two bugs of the original firmware are not reproduced. A write starting before a page held part-written in RAM and running through it left two buffers holding that page, and the older one was later programmed over the newer data; such a write now flushes the held page first. And its bad-block marker took two of its three metadata bytes from the stack, which can make a retired block look like a remap-log block; the marker is now written in full. Tested in a host simulator on NAND images of a Samsung YP-CP3 and a generic rk2705, against the original firmware's FTL object run under qemu-arm: identical traces of every read, program and erase, with a hash of the data each program writes, over mounting and reading, random writes with every sector verified, a power cut at every flash operation of a write, and a program or erase failure at every one. On a generic rk2705: the read-only mount reports the layout and capacities the original firmware does and every file's MD5 matches; a write test passes 8192/8192 across a remount and a power cycle. Co-Authored-By: Claude Opus 5.5 Change-Id: I573d944389cefb456ecf38c6c7b91bebfe0fe377 --- firmware/SOURCES | 1 + firmware/export/config.h | 8 + firmware/target/arm/rk27xx/ata-nand-rk27xx.c | 42 +- firmware/target/arm/rk27xx/ftl-rk27xx.c | 192 +- firmware/target/arm/rk27xx/ftl-scheme-a.c | 2136 ++++++++++++++++++ firmware/target/arm/rk27xx/ftl-scheme-a.h | 99 + firmware/target/arm/rk27xx/ftl-target.h | 27 +- 7 files changed, 2460 insertions(+), 45 deletions(-) create mode 100644 firmware/target/arm/rk27xx/ftl-scheme-a.c create mode 100644 firmware/target/arm/rk27xx/ftl-scheme-a.h diff --git a/firmware/SOURCES b/firmware/SOURCES index 81e4459bea..0bfa9f45cf 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1711,6 +1711,7 @@ target/arm/rk27xx/backlight-rk27xx.c target/arm/rk27xx/adc-rk27xx.c target/arm/rk27xx/sd-rk27xx.c target/arm/rk27xx/ftl-rk27xx.c +target/arm/rk27xx/ftl-scheme-a.c target/arm/rk27xx/flash-rk27xx.c target/arm/rk27xx/nand-rk27xx.c target/arm/rk27xx/usb-rk27xx.c diff --git a/firmware/export/config.h b/firmware/export/config.h index 3c4e164da1..508caf5e40 100644 --- a/firmware/export/config.h +++ b/firmware/export/config.h @@ -893,6 +893,14 @@ Lyre prototype 1 */ /* Storage related config handling */ +/* The rk27xx NAND's flash translation layer holds part-written pages in RAM + * (ftl-scheme-a.c) until a later write completes them; storage_flush() + * commits them at shutdown, ROLO and wherever else it is called. */ +#if (CONFIG_STORAGE & STORAGE_NAND) && (CONFIG_NAND == NAND_RK27XX) \ + && !defined(HAVE_STORAGE_FLUSH) +#define HAVE_STORAGE_FLUSH +#endif + #if (CONFIG_STORAGE & (CONFIG_STORAGE - 1)) != 0 /* Multiple storage drivers */ #define CONFIG_STORAGE_MULTI diff --git a/firmware/target/arm/rk27xx/ata-nand-rk27xx.c b/firmware/target/arm/rk27xx/ata-nand-rk27xx.c index ea493cd661..84f3c02fdd 100644 --- a/firmware/target/arm/rk27xx/ata-nand-rk27xx.c +++ b/firmware/target/arm/rk27xx/ata-nand-rk27xx.c @@ -28,26 +28,29 @@ #include "ftl-target.h" #include "nand-target.h" -uint32_t ftl_banks; -const struct nand_device_info_type* ftl_nand_type; -/* This file provides only STUBS for now */ - /** static, private data **/ static bool initialized = false; +/* The NAND is FTL_NUM_DRIVES drives - USER, and SYS when the target exposes + * it. storage.c hands us a drive index relative to our first, which is the + * FTL_DRIVE_* number. */ +#ifdef HAVE_MULTIDRIVE +#define NAND_DRIVE(d) (d) +#else +#define NAND_DRIVE(d) FTL_DRIVE_USER +#endif + /* API Functions */ int nand_read_sectors(IF_MD(int drive,) sector_t start, int incount, void* inbuf) { - (void)drive; - return ftl_read(start, incount, inbuf); + return ftl_read(NAND_DRIVE(IF_MD_DRV(drive)), start, incount, inbuf); } int nand_write_sectors(IF_MD(int drive,) sector_t start, int count, const void* outbuf) { - (void)drive; - return ftl_write(start, count, outbuf); + return ftl_write(NAND_DRIVE(IF_MD_DRV(drive)), start, count, outbuf); } void nand_spindown(int seconds) @@ -72,13 +75,20 @@ void nand_enable(bool on) void nand_get_info(IF_MD(int drive,) struct storage_info *info) { - (void)drive; - uint32_t ppb = ftl_banks * (*ftl_nand_type).pagesperblock; + int d = NAND_DRIVE(IF_MD_DRV(drive)); + + /* Capacity comes from the FTL's own tables, not from raw block + * geometry: Scheme A's usable size depends on the per-zone valid-block + * counts and on the system/user split recorded in ID block 1. */ (*info).sector_size = SECTOR_SIZE; - (*info).num_sectors = (*ftl_nand_type).userblocks * ppb; - (*info).vendor = ""; - (*info).product = ""; - (*info).revision = ""; + (*info).num_sectors = ftl_get_sectors(d); + (*info).vendor = "Rockchip"; +#ifdef HAVE_RK27XX_NAND_SYS + (*info).product = (d == FTL_DRIVE_SYS) ? "NAND SYS" : "NAND USER"; +#else + (*info).product = "NAND USER"; +#endif + (*info).revision = "1.0"; } long nand_last_disk_activity(void) @@ -109,7 +119,11 @@ int nand_num_drives(int first_drive) /* We don't care which logical drive number(s) we have been assigned */ (void)first_drive; +#ifdef HAVE_MULTIDRIVE + return FTL_NUM_DRIVES; +#else return 1; +#endif } #endif diff --git a/firmware/target/arm/rk27xx/ftl-rk27xx.c b/firmware/target/arm/rk27xx/ftl-rk27xx.c index 82a26b7865..e5e50a8cf6 100644 --- a/firmware/target/arm/rk27xx/ftl-rk27xx.c +++ b/firmware/target/arm/rk27xx/ftl-rk27xx.c @@ -7,6 +7,7 @@ * \/ \/ \/ \/ \/ * * Copyright (C) 2010 by Bertrik Sikken + * Copyright (C) 2026 by Marcin Bukat * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -17,39 +18,190 @@ * KIND, either express or implied. * ****************************************************************************/ - -#include "config.h" -#include "ftl-target.h" -/* this file provides empty STUBS for now */ +/* Rockbox storage on the rk27xx Scheme A FTL (ftl-scheme-a.c). + * + * Writing is opt-in: a build without FTL_ALLOW_WRITE mounts read-only and + * never writes the flash, not even the repairs a mount can make. */ + +#include "config.h" +#include "system.h" +#include "ftl-target.h" +#include "nand-target.h" +#include "flash-rk27xx.h" +#include "ftl-scheme-a.h" + +/* The boot ROM looks for ID blocks at every 512th raw sector of the boot + * area, up to 50 positions, by metadata type 0x69. */ +#define IDB_STRIDE 512 +#define IDB_POSITIONS 50 +#define IDB_META_TYPE 2 +#define IDB_TYPE 0x69 +#define IDB_MAX_BOOT_BLOCKS 64 + +static bool ftl_mounted = false; + +/* The size of the SYS volume, from ID block 1. + * + * ID block 0 is scrambled; ID block 1, the sector after it, is plain: + * + * +0 uint16_t LE blocks of bootloader + * +2 uint16_t LE SYS volume size, MB + * + * A sector marked 0x69 whose ID block 1 gives a sane block count and a SYS + * volume smaller than the chip is taken; descrambling ID block 0 to check its + * signature would buy little over that. Returns 0 if none is found. */ +static uint32_t idb_sys_sectors(void) +{ + const struct flash_geometry *geo = flash_get_geometry(); + uint8_t data[FLASH_SECTOR_SIZE], meta[FLASH_META_SIZE]; + uint32_t sectors = 0; + uint32_t pos; + + for (pos = 0; pos < IDB_POSITIONS && sectors == 0; pos++) + { + uint32_t raw = pos * IDB_STRIDE; + uint32_t blocks, mb; + + if (raw + 1 >= geo->total_sectors) + { + break; + } + + if (flash_read_raw(raw, data, meta) != 0 + || meta[IDB_META_TYPE] != IDB_TYPE) + { + continue; + } + if (flash_read_raw(raw + 1, data, meta) != 0) + { + continue; + } + + blocks = data[0] | (data[1] << 8); + mb = data[2] | (data[3] << 8); + + if (blocks > 0 && blocks <= IDB_MAX_BOOT_BLOCKS && mb > 0 && + mb * 2048 < geo->total_sectors) + { + sectors = mb * 2048; + } + } + return sectors; +} uint32_t ftl_init(void) { - /* TODO implement */ - return 0; + struct ftl_a_config config; + uint32_t ret = 0; + + flash_init(); + + if (flash_layer_init() != 0) + { + ret = 1; + } + else + { + config.sys_sectors = idb_sys_sectors(); +#ifdef FTL_ALLOW_WRITE + config.read_only = false; + /* the rk2705 NAND bootloader's generation formats with flag 1; its + * write logic is the same as the standard one's */ + config.alt_format_flag = 1; + config.alt_format_writable = true; +#else + config.read_only = true; + config.alt_format_flag = 1; + config.alt_format_writable = false; +#endif + + if (config.sys_sectors == 0) + { + ret = 2; /* without it USER cannot be told from SYS */ + } + else if (ftl_a_mount(&config) != FTL_A_OK) + { + ret = 3; + } + else if (ftl_a_capacity(FTL_A_VOL_USER) == 0) + { + ret = 4; + } + else + { + ftl_mounted = true; + } + } + return ret; } -uint32_t ftl_read(uint32_t sector, uint32_t count, void* buffer) +/* The FTL volume behind a drive. SYS is reachable only when exposed. */ +static int ftl_volume(int drive) { - /* TODO implement */ - (void)sector; - (void)count; - (void)buffer; - return 0; + int volume = FTL_A_VOL_USER; + +#ifdef HAVE_RK27XX_NAND_SYS + if (drive == FTL_DRIVE_SYS) + { + volume = FTL_A_VOL_SYS; + } +#else + (void)drive; +#endif + return volume; } -uint32_t ftl_write(uint32_t sector, uint32_t count, const void* buffer) +static bool drive_valid(int drive) { - /* TODO implement */ - (void)sector; - (void)count; - (void)buffer; - return 0; + return ftl_mounted && drive >= 0 && drive < FTL_NUM_DRIVES; +} + +uint32_t ftl_get_sectors(int drive) +{ + uint32_t sectors = 0; + + if (drive_valid(drive)) + { + sectors = ftl_a_capacity(ftl_volume(drive)); + } + return sectors; +} + +uint32_t ftl_read(int drive, uint32_t sector, uint32_t count, void *buffer) +{ + uint32_t ret = 1; + + if (drive_valid(drive)) + { + ret = ftl_a_read(ftl_volume(drive), sector, buffer, count) ? 2 : 0; + } + return ret; +} + +uint32_t ftl_write(int drive, uint32_t sector, uint32_t count, + const void *buffer) +{ + uint32_t ret = 1; + +#ifdef FTL_ALLOW_WRITE + if (drive_valid(drive)) + { + ret = ftl_a_write(ftl_volume(drive), sector, buffer, count) ? 2 : 0; + } +#else + /* refuse rather than pretend: a silent success would let the filesystem + * believe data was committed */ + (void)drive; (void)sector; (void)count; (void)buffer; +#endif + return ret; } uint32_t ftl_sync(void) { - /* TODO implement */ + if (ftl_mounted) + { + ftl_a_sync(); + } return 0; } - diff --git a/firmware/target/arm/rk27xx/ftl-scheme-a.c b/firmware/target/arm/rk27xx/ftl-scheme-a.c new file mode 100644 index 0000000000..5d71866aff --- /dev/null +++ b/firmware/target/arm/rk27xx/ftl-scheme-a.c @@ -0,0 +1,2136 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* THE SCHEME A FLASH TRANSLATION LAYER + * + * Many rk2705/rk2706 players store everything - their firmware and the + * user's music - on NAND flash managed by a block-mapping FTL, called Scheme + * A here to tell it from the log-structured layout later firmware uses. This + * file reads and writes that format exactly as the original firmware does, + * so a device keeps working with its original firmware after Rockbox has + * written to it. Where the original firmware's behaviour is odd, it is + * reproduced on purpose and marked as such. + * + * + * VOLUMES + * + * The FTL presents one linear space of 512-byte logical sectors, split into + * two volumes: + * + * SYS the first sectors. The original firmware keeps itself here + * (BASE.RKW and its resources) as a FAT volume of its own. + * USER everything after SYS: the music storage, the drive the original + * firmware exports over USB. + * + * Where SYS ends is not in the FTL's own tables: the boot area's ID block + * records it (see ftl-rk27xx.c), and the caller passes it in. + * + * + * BLOCKS AND ZONES + * + * The unit of mapping is the SUPER-BLOCK - on a two-plane chip, a physical + * block from each plane, used as one (flash-rk27xx.h). Blocks are grouped in + * ZONES of 256; a 4096-block chip has 16. Mapping never crosses a zone: a + * logical block always lives somewhere in its own zone, so a zone can be + * described by one byte per block. + * + * The first blocks of the chip are the BOOT AREA: the bootloader and the ID + * blocks the boot ROM looks for. The FTL does not use them. The zone they are + * in is the SYSTEM ZONE (zone 0 on every device seen), and the number of boot + * blocks in it is the SYSTEM OFFSET. + * + * + * THE ZONE TABLE + * + * Each zone is described by a 512-byte table. Its first 256 bytes are block + * positions within the zone: + * + * [0 .. n-1] logical block i is stored at position table[i]; + * n is the zone's block count (see below) + * [base - 1] system zone only: the exchange-record block + * [base .. base+5] the exchange pool: six erased blocks, one per + * exchange slot, that writes go to + * [base + 6] the spare block power-loss recovery merges into + * [0xfe], [0xff] the zone's two remap-log blocks: mirror, primary + * + * Positions not listed are bad or unused. The table is a permutation - no + * block appears twice - and writing only ever swaps entries. + * + * In the system zone every index above except the log entries' and the + * format flag's counts DOWN from the top shifted by the system offset: the + * top entries are taken by the boot blocks. So the primary log block of the + * system zone is at table[0xff - offset], its pool at table[base - offset]. + * + * The BASE, the first pool entry, depends on the firmware build that + * formatted the device - 0xf7 on the rk2705 build, 0xf1 on the Samsung + * YP-CP3 - as does the system offset. Neither is stored anywhere. Both are + * recovered from the structure at mount (probe_layout), and a device whose + * layout cannot be confirmed is mounted read-only: writing with the wrong + * base would erase blocks holding data. + * + * Bytes 0x100..0x10b are the SWAP LOG: one (old, new) position pair per + * exchange slot, recording the last swap that slot made. In the system + * zone's table, byte 0xff (unshifted) is the FORMAT FLAG - 2 for a formatted + * device - and the ten entries below the exchange record, table[base - 11 .. + * base - 2], are the device's REPLACEMENT BLOCKS for bad-block relocation. + * + * + * THE REMAP LOG + * + * A zone's table is never rewritten in place. Every version is appended, one + * sector per page, to the zone's remap-log block; the newest programmed page + * is the current table. The first page of a log block carries the block type + * 0x52 in its metadata, which is how a mount finds it. Every version is + * written twice, to the primary and to the mirror log block, so that + * recycling a full log - erasing both and starting again at page 0 - cannot + * lose the zone: an interrupted recycle leaves one of them intact, and the + * mount rewrites the other from it. + * + * + * THE EXCHANGE RECORD + * + * One block of the system zone holds device-wide state, one version per + * page: byte z of the newest page is zone z's BLOCK COUNT - how many logical + * blocks it holds - and from byte 0x40, ten little-endian 16-bit entries + * list the blocks that have been replaced after failing. The block counts + * give the capacity: every logical block is a super-block of data. + * + * + * WRITING + * + * NAND is erased a block at a time and programmed a page at a time, only + * once between erases. A write to a logical block therefore goes to a + * different, erased physical block - an EXCHANGE BLOCK - through one of six + * exchange slots per zone: + * + * 1. open: swap the logical block's table entry with the slot's pool + * entry, record the swap in the swap log, erase the new block, and + * publish the table - all before any data is written + * 2. copy forward the pages before the first one being written, from the + * old block; then write pages in order, the slot's cursor advancing + * 3. close: copy forward the rest of the old block + * + * The old block is now in the pool, to be erased when a later write needs + * it. A full exchange block is a finished write. Pages are staged in three + * page-sized RAM buffers, so sectors written one at a time reach the flash a + * page at a time; ftl_a_sync() writes them out. + * + * + * POWER LOSS + * + * There is no journal. Because the table is published before the data, a + * write cut short leaves a published table pointing at an exchange block + * that is not full, and a swap-log entry whose old and new differ. At every + * writable mount, recovery finds such slots, finds the last page programmed + * in the exchange block, and merges those pages with the rest of the old + * block into the zone's spare block, then publishes the table with the + * swap undone. Everything before the final publish can be repeated, so + * losing power during recovery only means recovering again. + * + * Whatever is still in the RAM buffers when power fails is lost. + * + * + * BAD BLOCKS + * + * A block is bad when metadata byte 0 of its first page is not 0xff. When a + * program or erase fails, the block is marked bad and replaced by the next + * of the ten replacement blocks, the replacement recorded in the exchange + * record. Blocks that are bad from the factory are simply never in a table. + * + * + * METADATA + * + * Of each sector's spare bytes the FTL uses three: + * + * [0] 0xff = good block (first page only) + * [1] 0x00 = this page is programmed - written by the flash layer on every + * program, so an erased page reads 0xff + * [2] block type: 0x52 remap-log block; 0x69 marks the boot area's ID + * blocks + */ + +#include +#include +#include + +#include "flash-rk27xx.h" +#include "ftl-scheme-a.h" + +/* ---- layout ---- */ + +#define ZONE_BLOCKS 256 /* blocks per zone */ +#define MAX_ZONES 64 +#define EXCH_SLOTS 6 /* exchange slots per zone */ +#define REPLACEMENT_SLOTS 10 /* bad-block replacements, device-wide */ +#define WRITE_CACHE_PAGES 3 /* page staging buffers */ + +#define TABLE_SIZE 512 +#define TBL_LOG_PRIMARY 0xff /* position of the primary log block */ +#define TBL_LOG_MIRROR 0xfe /* ... and of its mirror */ +#define TBL_SWAP_LOG 0x100 /* EXCH_SLOTS (old, new) pairs */ +#define TBL_FORMAT_FLAG 0xff /* system zone only, never shifted */ + +/* In the exchange record: the replaced-block list */ +#define XREC_REPLACED 0x40 + +/* The original firmware's reserve base, and the one used before a mount has + * derived the device's own */ +#define DEFAULT_RESERVE_BASE 0xf7 + +/* The format flag of a formatted device */ +#define FORMAT_FLAG 2 + +/* ---- metadata ---- */ + +#define META_BAD 0 /* [0]: 0xff = good block */ +#define META_PROGRAMMED 1 /* [1]: 0x00 once the page is programmed */ +#define META_TYPE 2 /* [2]: block type */ +#define META_ERASED 0xff +#define META_TYPE_LOG 0x52 + +static const uint8_t log_block_meta[FLASH_META_SIZE] = + { 0xff, 0xff, META_TYPE_LOG }; +static const uint8_t bad_block_meta[FLASH_META_SIZE] = + { 0x00, 0xff, 0xff }; + +#define NO_ZONE 0xffff +#define NO_REPLACEMENT 0xffff /* an unused replaced-block entry */ +#define NO_LBA 0xffffffff + +/* ---- state ---- */ + +/* An exchange slot: a logical block being rewritten into an exchange block. */ +struct exch_slot +{ + uint32_t cursor; /* next sector to write in the exchange block */ + uint32_t end; /* the sector after the exchange block */ + uint32_t src; /* first sector of the block being replaced */ + uint8_t age; /* EXCH_SLOTS - 1 = used last, 0 = reused next */ + bool open; +}; + +struct zone +{ + uint8_t log_block; /* position of the primary remap-log block */ + uint16_t log_page; /* page of the newest table version in it */ + uint8_t block_count; /* logical blocks in the zone */ + struct exch_slot exch[EXCH_SLOTS]; +}; + +/* A page being assembled in RAM. */ +struct write_cache +{ + uint8_t buf[FLASH_MAX_SEC_PER_PAGE * FLASH_SECTOR_SIZE]; + bool valid; + uint8_t age; /* as exch_slot.age */ + uint32_t next_lba; /* the page holds [page start, next_lba) */ + struct exch_slot *slot; /* the exchange slot the page is for */ +}; + +static struct +{ + const struct flash_geometry *geo; + struct ftl_a_config config; + bool mounted; + + /* the layout of this device */ + uint8_t reserve_base; + uint16_t boot_blocks; /* (system zone << 8) | system offset */ + uint32_t logical_sectors; /* both volumes */ + uint16_t replaced[REPLACEMENT_SLOTS]; + struct zone zones[MAX_ZONES]; + + /* what the last mount found */ + enum ftl_a_error error; + enum ftl_a_confidence confidence; + bool write_protect; + uint8_t base_fits; + uint8_t format_flag; + + /* the table of cached_zone, the one most recently used */ + uint16_t cached_zone; + uint8_t table[TABLE_SIZE]; + + struct write_cache cache[WRITE_CACHE_PAGES]; +} ftl = +{ + .reserve_base = DEFAULT_RESERVE_BASE, + .write_protect = true, +}; + +/* Scratch sector for the exchange record and for the system zone's table, + * shared by relocate_block() and replaced_block(); neither needs it across a + * call to the other. */ +static uint8_t scratch[TABLE_SIZE]; + +/* ---- geometry helpers ---- */ + +static uint8_t sec_per_page_raw(void) +{ + return ftl.geo->sec_per_page_raw; +} + +static uint8_t sec_per_page(void) +{ + return ftl.geo->sec_per_page; +} + +static uint16_t sec_per_block(void) +{ + return ftl.geo->sec_per_block; +} + +static uint8_t system_zone(void) +{ + return (uint8_t)(ftl.boot_blocks >> 8); +} + +static uint8_t system_offset(void) +{ + return (uint8_t)ftl.boot_blocks; +} + +/* How far the table indices of `zone` are shifted down by boot blocks. */ +static uint8_t table_shift(uint16_t zone) +{ + uint8_t shift = 0; + + if (zone <= system_zone()) + { + shift = system_offset(); + } + return shift; +} + +/* Table index of the exchange record (meaningful in the system zone). */ +static uint8_t xrec_index(void) +{ + return (uint8_t)(ftl.reserve_base - 1 - system_offset()); +} + +/* First sector of block `blk`, (zone << 8) | position. Past the end of the + * chip it returns the sector past the end, which the flash layer refuses. */ +static uint32_t block_sector(uint16_t blk) +{ + uint32_t sec = ftl.geo->total_sectors; + + if (blk < ftl.geo->total_blocks) + { + sec = (uint32_t)blk * sec_per_block(); + } + return sec; +} + +/* Sector of the system zone's current table. */ +static uint32_t system_table_sector(void) +{ + const struct zone *sz = &ftl.zones[system_zone()]; + + return block_sector((uint16_t)(sz->log_block + (system_zone() << 8))) + + (uint32_t)sec_per_page_raw() * sz->log_page; +} + +static void set_write_protect(bool protect) +{ + ftl.write_protect = protect; + flash_set_writable(!protect); +} + +static void reset_state(void) +{ + int i, j; + + for (i = 0; i < WRITE_CACHE_PAGES; i++) + { + ftl.cache[i].valid = false; + ftl.cache[i].age = (uint8_t)i; + ftl.cache[i].next_lba = NO_LBA; + } + + ftl.cached_zone = NO_ZONE; + + for (i = 0; i < MAX_ZONES; i++) + { + for (j = 0; j < EXCH_SLOTS; j++) + { + ftl.zones[i].exch[j].open = false; + ftl.zones[i].exch[j].age = (uint8_t)j; + } + } +} + +/* Sectors on a volume. USER is whatever SYS leaves. */ +static uint32_t volume_sectors(int volume) +{ + uint32_t sectors = 0; + + if (volume == FTL_A_VOL_SYS) + { + sectors = ftl.config.sys_sectors; + } + else if (volume == FTL_A_VOL_USER) + { + sectors = ftl.logical_sectors - ftl.config.sys_sectors; + } + return sectors; +} + +/* ---- logical addresses ---- */ + +/* Split a logical sector into its zone, logical block in the zone and sector + * in the block. Zones hold different numbers of logical blocks, so this + * walks the block counts. */ +static void locate(uint32_t lba, uint16_t *zone, uint8_t *block, + uint16_t *sector) +{ + uint32_t blk = lba / sec_per_block(); + uint16_t z; + + *sector = (uint16_t)(lba % sec_per_block()); + + for (z = 0; z < MAX_ZONES && blk >= ftl.zones[z].block_count; z++) + { + blk -= ftl.zones[z].block_count; + } + + *block = (uint8_t)blk; + *zone = z; +} + +/* First logical sector of `zone`. A zone past the last holding any blocks + * returns the capacity. */ +static uint32_t zone_first_lba(uint16_t zone) +{ + uint32_t lba = 0; + uint16_t z = 0; + + while (z != zone && z < MAX_ZONES && lba < ftl.logical_sectors) + { + lba += (uint32_t)ftl.zones[z].block_count * sec_per_block(); + z++; + } + return lba; +} + +/* ---- bad blocks ---- */ + +/* Is the block containing `sec` marked bad? On a two-plane part both planes' + * markers count - but, as the original firmware does, only the first read's + * status decides whether they are consulted at all. */ +static bool block_is_bad(uint32_t sec) +{ + uint32_t addr = sec - sec % sec_per_block(); + uint8_t meta[FLASH_META_SIZE]; + uint8_t marker = 1; + + if (flash_read(addr, NULL, meta, 1) == 0) + { + marker = meta[META_BAD]; + if (ftl.geo->planes > 1) + { + flash_read(addr + sec_per_page_raw(), NULL, meta, 1); + marker &= meta[META_BAD]; + } + } + return marker != META_ERASED; +} + +static void mark_block_bad(uint32_t sec) +{ + uint32_t row = sec - sec % sec_per_block(); + + flash_erase(row); + flash_program(row, NULL, bad_block_meta, 1); + + if (ftl.geo->planes > 1) + { + row += sec_per_page_raw(); + flash_program(row, NULL, bad_block_meta, 1); + } +} + +/* The block that `blk` has been replaced with, following replacements of + * replacements; `blk` itself when it has none. */ +static uint32_t replaced_block(uint32_t blk) +{ + uint32_t result = blk; + int i; + + for (i = 0; i < REPLACEMENT_SLOTS && ftl.replaced[i] != NO_REPLACEMENT; i++) + { + if (ftl.replaced[i] == (uint16_t)result) + { + flash_read(system_table_sector(), scratch, NULL, 1); + result = scratch[xrec_index() + i - REPLACEMENT_SLOTS] + + (system_zone() << 8); + } + } + return result; +} + +/* Replace the block containing `sec` after a program or erase failed in it, + * and return the sector in the replacement that corresponds to `sec`. + * + * With `data`, a write of `n` sectors at `sec` is carried over: the sectors + * of the block before `sec` are copied, then the data programmed. The + * replacement is recorded in the exchange record, any exchange slot writing + * into the failed block is moved to the replacement, and the failed block is + * marked bad. When every replacement is used up, `sec` itself is returned. + * + * The original firmware records replacements but, reading the exchange + * record back at mount, looks for them on a page it never writes them to: + * they are forgotten at every reboot. That is kept - see mount_scan(). */ +static uint32_t relocate_block(uint32_t sec, const void *data, uint16_t n) +{ + uint32_t result = sec; + uint32_t in_block = sec % sec_per_block(); + uint16_t failed_blk; + uint32_t xrec; + int slot; + + if (ftl.replaced[REPLACEMENT_SLOTS - 1] == NO_REPLACEMENT) + { + failed_blk = (uint16_t)((sec < ftl.geo->total_sectors) ? + sec / sec_per_block() : ftl.geo->total_blocks); + + flash_read(system_table_sector(), scratch, NULL, 1); + xrec = block_sector((uint16_t)(scratch[xrec_index()] + + (system_zone() << 8))); + + for (slot = 0; slot < REPLACEMENT_SLOTS; slot++) + { + uint32_t dest; + bool retargeted = false; + int z, k; + + if (ftl.replaced[slot] != NO_REPLACEMENT) + { + continue; + } + + dest = block_sector((uint16_t)((system_zone() << 8) + + scratch[xrec_index() + slot - REPLACEMENT_SLOTS])); + + if (data != NULL) + { + flash_copy(sec - in_block, dest, (uint16_t)in_block); + dest += in_block; + + if (n != 0) + { + if (ftl.geo->planes > 1 && n != sec_per_page_raw()) + { + flash_program_page(dest, data, NULL); + dest += sec_per_page(); + } + else + { + flash_program(dest, data, NULL, sec_per_page_raw()); + dest += sec_per_page_raw(); + } + } + } + + /* append the new list to the exchange record */ + flash_read(xrec + slot * sec_per_page_raw(), scratch, NULL, 1); + scratch[XREC_REPLACED + slot * 2] = (uint8_t)failed_blk; + scratch[XREC_REPLACED + slot * 2 + 1] = (uint8_t)(failed_blk >> 8); + flash_program(xrec + (slot + 1) * sec_per_page_raw(), scratch, + NULL, 1); + + /* an exchange slot writing into the failed block follows it */ + for (z = 0; z < MAX_ZONES && !retargeted; z++) + { + for (k = 0; k < EXCH_SLOTS && !retargeted; k++) + { + struct exch_slot *s = &ftl.zones[z].exch[k]; + uint32_t block_start = s->cursor & ~(sec_per_block() - 1); + + if (s->open && block_start <= sec && s->end > sec) + { + uint32_t dest_start = dest & ~(sec_per_block() - 1); + + s->cursor = dest_start + s->cursor % sec_per_block(); + s->end = dest_start + sec_per_block(); + retargeted = true; + } + } + } + + mark_block_bad(sec); + ftl.replaced[slot] = failed_blk; + result = dest; + break; + } + } + return result; +} + +/* ---- the zone table ---- */ + +/* Bring `zone`'s current table into ftl.table. */ +static void load_table(uint16_t zone) +{ + if (ftl.cached_zone != zone) + { + struct zone *z = &ftl.zones[zone]; + + ftl.cached_zone = zone; + flash_read(block_sector((uint16_t)((zone << 8) + z->log_block)) + + z->log_page * sec_per_page_raw(), + ftl.table, NULL, 1); + } +} + +/* Where logical sector `lba` is committed: its block per the table, ignoring + * any exchange block it is being rewritten into. */ +static uint32_t map_committed(uint32_t lba) +{ + uint16_t zone, sector; + uint8_t block; + uint32_t blk; + + locate(lba, &zone, &block, §or); + load_table(zone); + + blk = replaced_block((zone << 8) + ftl.table[block]); + return block_sector((uint16_t)blk) | sector; +} + +/* Where logical sector `lba` can be read now. A sector in a block being + * rewritten reads from the block being replaced until the exchange block's + * cursor has passed it; past the cursor the exchange block is erased. */ +static uint32_t map_current(uint32_t lba) +{ + uint16_t zone, sector; + uint8_t block; + uint32_t addr; + int i; + + locate(lba, &zone, &block, §or); + load_table(zone); + + addr = replaced_block((zone << 8) + ftl.table[block]); + addr = block_sector((uint16_t)addr) | sector; + + for (i = 0; i < EXCH_SLOTS; i++) + { + struct exch_slot *s = &ftl.zones[zone].exch[i]; + + if (s->open && s->cursor <= addr && s->end > addr) + { + addr = s->src + addr % sec_per_block(); + break; + } + } + return addr; +} + +/* Append ftl.table as the new version of the table of the zone holding + * `lba`, to both log blocks. A full log is erased and restarted. */ +static void publish_table(uint32_t lba) +{ + uint16_t zone, sector; + uint8_t block; + struct zone *z; + uint32_t primary, mirror; + + locate(lba, &zone, &block, §or); + z = &ftl.zones[zone]; + + ftl.cached_zone = zone; + z->log_page++; + + mirror = (zone << 8) + + ftl.table[(uint8_t)(TBL_LOG_MIRROR - table_shift(zone))]; + primary = replaced_block(z->log_block + (zone << 8)); + mirror = replaced_block(mirror); + + primary = block_sector((uint16_t)primary); + mirror = block_sector((uint16_t)mirror); + + if (z->log_page < sec_per_block() / sec_per_page_raw()) + { + uint32_t page = z->log_page * sec_per_page_raw(); + + flash_program(page + primary, ftl.table, NULL, 1); + flash_program(page + mirror, ftl.table, NULL, 1); + } + else + { + z->log_page = 0; + + if (flash_erase(primary) != 0) + { + primary = relocate_block(primary, NULL, 0); + } + flash_program(primary, ftl.table, log_block_meta, 1); + + if (flash_erase(mirror) != 0) + { + mirror = relocate_block(mirror, NULL, 0); + } + flash_program(mirror, ftl.table, log_block_meta, 1); + } +} + +/* ---- exchange slots ---- */ + +/* Finish the rewrite a slot is doing: copy the rest of the old block into + * the exchange block, and free the slot. */ +static void exch_close(struct exch_slot *s) +{ + if (s->open) + { + if (s->cursor != s->end) + { + uint32_t from = s->src + s->cursor % sec_per_block(); + uint16_t count = (uint16_t)(s->end - s->cursor); + + if (flash_copy(from, s->cursor, count) != 0) + { + uint32_t dest = relocate_block(s->cursor, NULL, 0); + + if (dest != s->cursor) + { + flash_copy(from, dest, count); + } + } + } + s->open = false; + } +} + +/* Start rewriting the logical block holding `lba`: close any slot already + * writing into its current block, take the least recently used slot, swap + * the slot's pool block in, erase it, and publish the table. Returns the + * slot's index. + * + * The slot ages form a permutation of 0 .. EXCH_SLOTS-1, so there always is + * a slot of age 0 to take. */ +static int exch_open(uint32_t lba) +{ + uint16_t zone, sector; + uint8_t block, pool, fresh; + struct exch_slot *s = NULL; + uint32_t addr; + int i; + + locate(lba, &zone, &block, §or); + + for (i = 0; i < EXCH_SLOTS; i++) + { + s = &ftl.zones[zone].exch[i]; + + if (s->open) + { + addr = map_committed(lba) & ~(sec_per_block() - 1); + if ((s->cursor & ~(sec_per_block() - 1)) == addr) + { + exch_close(s); + } + } + } + + for (i = 0; i < EXCH_SLOTS; i++) + { + s = &ftl.zones[zone].exch[i]; + + if (!s->open || s->age == 0) + { + exch_close(s); + break; + } + } + + /* swap the pool block in; the swap log records (old, new) */ + pool = (uint8_t)(ftl.reserve_base - table_shift(zone) + i); + fresh = ftl.table[pool]; + ftl.table[pool] = ftl.table[block]; + ftl.table[TBL_SWAP_LOG + i * 2] = ftl.table[block]; + ftl.table[TBL_SWAP_LOG + i * 2 + 1] = fresh; + ftl.table[block] = fresh; + + addr = replaced_block((zone << 8) + fresh); + addr = block_sector((uint16_t)addr); + + if (flash_erase(addr) != 0) + { + addr = relocate_block(addr, NULL, 0); + } + + s->cursor = addr; + s->end = addr + sec_per_block(); + s->open = true; + + publish_table(lba); + + return i; +} + +/* The exchange slot sector `lba` - the first of a page - is written through, + * opening one if needed. Pages are written in order: if the slot's cursor is + * behind `lba`, the pages in between are copied forward from the old block + * first. */ +static struct exch_slot *exch_for_write(uint32_t lba) +{ + uint16_t zone, sector; + uint8_t block; + uint32_t addr, in_block; + struct exch_slot *s = NULL; + int i; + + locate(lba, &zone, &block, §or); + addr = map_committed(lba); + + for (i = 0; i < EXCH_SLOTS; i++) + { + s = &ftl.zones[zone].exch[i]; + + if (!s->open || s->cursor > addr || s->end <= addr) + { + continue; + } + + if (s->cursor != addr) + { + uint16_t count = (uint16_t)(addr - s->cursor); + + in_block = s->cursor % sec_per_block(); + if (flash_copy(s->src + in_block, s->cursor, count) != 0) + { + uint32_t dest = relocate_block(s->cursor, NULL, 0); + + if (dest != s->cursor) + { + flash_copy(s->src + in_block, dest, count); + addr = addr - s->cursor + dest; + } + } + s->cursor = addr; + } + break; + } + + if (i >= EXCH_SLOTS) + { + i = exch_open(lba); + s = &ftl.zones[zone].exch[i]; + + in_block = addr % sec_per_block(); + s->src = addr - in_block; + + if (in_block != 0) + { + if (flash_copy(s->src, s->cursor, (uint16_t)in_block) != 0) + { + uint32_t dest = relocate_block(s->cursor, NULL, 0); + + if (dest != s->cursor) + { + flash_copy(s->src, dest, (uint16_t)in_block); + s->cursor = dest; + } + } + } + + s->cursor += in_block; + } + + for (i = 0; i < EXCH_SLOTS; i++) + { + struct exch_slot *other = &ftl.zones[zone].exch[i]; + + if (other->age > s->age) + { + other->age--; + } + } + s->age = EXCH_SLOTS - 1; + + return s; +} + +/* ---- power-loss recovery ---- */ + +/* The number of sectors of an exchange block that hold data: everything up + * to the last programmed page. A last page that does not read back cleanly + * was cut short by the power loss and is not counted. + * + * That back-off computes page & ~sec_per_page, clearing a single bit, where + * aligning down to a page would need page & ~(sec_per_page - 1). It is the + * original firmware's arithmetic, kept so that recovery merges exactly the + * pages the original firmware would. */ +static int32_t exch_written(uint32_t src) +{ + uint8_t meta[FLASH_META_SIZE]; + int32_t page = (int16_t)(sec_per_block() - sec_per_page_raw()); + bool done = false; + + while (!done) + { + flash_read(src + page, NULL, meta, 1); + + if (meta[META_PROGRAMMED] != 0) + { + page = (int16_t)(page - sec_per_page_raw()); + done = page < 0; + } + else + { + if (flash_read(src + page, NULL, NULL, sec_per_page_raw()) != 0) + { + page = (int16_t)((page & ~sec_per_page()) - sec_per_page_raw()); + } + done = true; + } + } + + return (int16_t)(page + sec_per_page_raw()); +} + +/* Finish, in every zone, the writes a power loss interrupted - see POWER + * LOSS above. For each swap-log entry still open, the written pages of the + * exchange block and the rest of the old block are merged into the zone's + * spare block, which takes the logical block's place; the exchange block + * returns to the pool and the old block becomes the new spare. */ +static void recover_interrupted(void) +{ + uint32_t capacity = volume_sectors(FTL_A_VOL_SYS) + + volume_sectors(FTL_A_VOL_USER); + uint32_t zone_blk = 0; + bool done = false; + + while (!done && zone_blk < ftl.geo->total_blocks) + { + uint16_t zone = (uint16_t)(zone_blk >> 8); + int slot; + + if (zone_blk + ZONE_BLOCKS <= ftl.boot_blocks) + { + zone_blk += ZONE_BLOCKS; /* all boot area */ + continue; + } + + if (zone_first_lba(zone) >= capacity) + { + done = true; /* no zone after this holds data */ + continue; + } + + map_current(zone_first_lba(zone)); /* loads the zone's table */ + + for (slot = 0; slot < EXCH_SLOTS; slot++) + { + uint8_t old = ftl.table[TBL_SWAP_LOG + slot * 2]; + uint8_t new = ftl.table[TBL_SWAP_LOG + slot * 2 + 1]; + uint8_t pool, spare, spare_pos; + uint32_t exch, dest, orig, n; + int32_t written; + uint8_t at; + + if (old == new) + { + continue; /* closed */ + } + + exch = block_sector((uint16_t)replaced_block(zone_blk + new)); + written = exch_written(exch); + + if (sec_per_block() <= written) + { + continue; /* full: the write completed */ + } + + spare = (uint8_t)(ftl.reserve_base + EXCH_SLOTS + - table_shift(zone)); + pool = (uint8_t)(ftl.reserve_base - table_shift(zone)); + + /* undo the swap: the spare block takes the logical block's + * place, the exchange block goes back to the pool, the old + * block becomes the spare */ + spare_pos = ftl.table[spare]; + for (at = 0; at < pool && ftl.table[at] != new; at++) + { + } + ftl.table[pool + slot] = ftl.table[at]; + ftl.table[at] = spare_pos; + ftl.table[spare] = old; + + ftl.table[TBL_SWAP_LOG + slot * 2] = 0xff; + ftl.table[TBL_SWAP_LOG + slot * 2 + 1] = 0xff; + + dest = block_sector((uint16_t)replaced_block(zone_blk + spare_pos)); + orig = block_sector((uint16_t)replaced_block(zone_blk + old)); + + flash_erase(dest); + + /* the written pages, where they read back; the old block's + * where they do not */ + for (n = 0; n < (uint32_t)written; n += sec_per_page_raw()) + { + if (flash_read(exch + n, NULL, NULL, sec_per_page_raw()) != 0) + { + flash_copy(orig + n, dest + n, sec_per_page_raw()); + } + else + { + flash_copy(exch + n, dest + n, sec_per_page_raw()); + } + } + flash_copy(orig + written, dest + written, + (uint16_t)(sec_per_block() - written)); + + publish_table(zone_first_lba(zone)); + } + + zone_blk += ZONE_BLOCKS; + } +} + +/* ---- recognising the layout ---- + * + * The reserve base and the system offset differ between the firmware builds + * that format devices, and neither is recorded. Both follow from the + * structure: + * + * system offset the two log blocks are always the top two table entries, + * shifted down by the offset in the system zone - so the + * offset is 0xff less the index of the primary log block's + * own position in its table + * base opening a slot i parks the displaced block at + * table[base + i] and records it in the swap log, so a used + * slot gives base = index of its old block - i; and, needing + * no write history, the system zone's table[base - 1] must + * name an exchange record whose block counts are all at + * most base, peaking at exactly base + * + * The second test decides, the first is only a hint. Only a unique answer, + * cross-checked against every zone's log blocks, makes the device writable: + * a wrong base or offset would publish tables that erase live data. */ + +/* Find a zone's log blocks - metadata type 0x52 - and the newest programmed + * page of the first. Returns how many were found: 2 normally, 1 when a + * power loss during a log recycle left one erased, 0 for none. */ +static int probe_find_log(uint16_t zone_base, uint8_t *pos_a, uint8_t *pos_b, + uint16_t *newest) +{ + uint8_t meta[FLASH_META_SIZE]; + uint16_t pages = sec_per_block() / sec_per_page_raw(); + int pos, i, found = 0; + + *pos_a = 0xff; + *pos_b = 0xff; + *newest = 0; + + for (pos = 0xff; pos >= 0 && found < 2; pos--) + { + uint32_t sec = block_sector((uint16_t)(zone_base + pos)); + + if (flash_read(sec, NULL, meta, 1) == 0 + && meta[META_TYPE] == META_TYPE_LOG) + { + if (found == 0) + { + *pos_a = (uint8_t)pos; + } + else + { + *pos_b = (uint8_t)pos; + } + found++; + } + } + + if (found > 0) + { + uint32_t base = block_sector((uint16_t)(zone_base + *pos_a)); + + for (i = 0; i < (int)pages; i++) + { + if (flash_read(base + (uint32_t)i * sec_per_page_raw(), + NULL, meta, 1) != 0) + { + break; + } + if (meta[META_PROGRAMMED] != META_ERASED) + { + *newest = (uint16_t)i; + } + } + } + return found; +} + +/* The base the swap log points to. -1 with no evidence, -2 if slots + * disagree. Only values that occur once in the table count: a formatted + * system zone repeats some. */ +static int probe_base(const uint8_t *tbl, int *votes) +{ + int i, j, base = -1; + bool conflict = false; + + *votes = 0; + for (i = 0; i < EXCH_SLOTS && !conflict; i++) + { + uint8_t old = tbl[TBL_SWAP_LOG + i * 2]; + uint8_t new = tbl[TBL_SWAP_LOG + i * 2 + 1]; + int at = -1, count = 0; + + if (old == new) + { + continue; /* unused slot */ + } + + for (j = 0; j < ZONE_BLOCKS; j++) + { + if (tbl[j] == old) + { + count++; + at = j; + } + } + if (count != 1 || at - i < 0) + { + continue; + } + + if (base >= 0 && base != at - i) + { + conflict = true; + } + else + { + base = at - i; + (*votes)++; + } + } + + if (conflict) + { + base = -2; + } + else if (*votes == 0) + { + base = -1; + } + return base; +} + +/* The system offset from where the log blocks sit. -1 if they do not sit as + * the top two entries of some shift. */ +static int probe_sys_offset(const uint8_t *tbl, uint8_t log_primary, + uint8_t log_mirror) +{ + int i, at = -1, count = 0, offset = -1; + + for (i = 0; i < ZONE_BLOCKS; i++) + { + if (tbl[i] == log_primary) + { + count++; + at = i; + } + } + if (count == 1 && at >= 1 && tbl[at - 1] == log_mirror) + { + offset = TBL_LOG_PRIMARY - at; + } + return offset; +} + +/* Does `base` make the system zone's exchange record a real one? Its block + * counts must all be at most base, and reach it: + * + * CP3 d9 f1 f1 f1 f1 ... f1 base f1 + * rk2705 df f6 f6 f7 f6 f7 ... f7 f4 base f7 + * + * A wrong base names some other block, whose bytes are not that. + * The newest page is found as mount_scan() finds it, including ignoring the + * read status: erased pages fail ECC, and only byte 0 decides. */ +static bool probe_base_fits(uint8_t base, const uint8_t *sys_tbl, + uint8_t sys_zone, uint8_t sys_offset, + uint16_t nzones, uint8_t *page) +{ + uint32_t addr; + int idx, z, max = -1; + bool fits = true; + + addr = block_sector((uint16_t)(sys_tbl[(uint8_t)(base - 1 - sys_offset)] + + (sys_zone << 8))); + + for (idx = REPLACEMENT_SLOTS - 1; ; idx--) + { + flash_read(addr + (uint32_t)idx * sec_per_page_raw(), page, NULL, 1); + if (page[0] != META_ERASED || idx == 0) + { + break; + } + } + + for (z = 0; z < nzones && fits; z++) + { + if (page[z] > base) + { + fits = false; + } + else if (page[z] > max) + { + max = page[z]; + } + } + return fits && max == base; +} + +/* Read zone z's table version `newest` from log block `log_blk`. */ +static int probe_read_table(uint16_t z, uint8_t log_blk, uint16_t newest, + uint8_t *tbl) +{ + return flash_read(block_sector((uint16_t)((z << 8) + log_blk)) + + (uint32_t)newest * sec_per_page_raw(), + tbl, NULL, 1); +} + +/* With one log block left, could the system offset be `so`? The top `so` + * entries of the system zone's table are the boot blocks, recorded as + * positions below `so` (formatting zeroes them, so they repeat), and the + * lost log block's position cannot be one of them. */ +static bool probe_layout_fits(const uint8_t *tbl, int so, + bool survivor_is_primary) +{ + int j; + bool fits = true; + uint8_t other; + + for (j = ZONE_BLOCKS - so; j <= TBL_LOG_PRIMARY; j++) + { + if (tbl[j] >= so) + { + fits = false; + } + } + + other = survivor_is_primary ? tbl[TBL_LOG_MIRROR - so] + : tbl[TBL_LOG_PRIMARY - so]; + return fits && other >= so; +} + +/* How many bases fit if the system zone is `sz` with offset `so`; *found + * gets the last that did. The pool and the spare must fit under the log + * blocks, and table[base - 1 - so] must be a real entry. */ +static int probe_base_search(const uint8_t *sys_tbl, uint8_t sz, uint8_t so, + uint16_t nzones, int *found) +{ + static uint8_t page[TABLE_SIZE]; + int b, fits = 0; + + for (b = so + 1; b + EXCH_SLOTS < TBL_LOG_MIRROR; b++) + { + if (probe_base_fits((uint8_t)b, sys_tbl, sz, so, nzones, page)) + { + fits++; + *found = b; + } + } + return fits; +} + +/* Recognise the layout of `nzones` zones - see probe_layout(). */ +static bool probe_zones(uint16_t nzones) +{ + static uint8_t tbl[TABLE_SIZE]; + uint8_t log_a[MAX_ZONES], log_b[MAX_ZONES]; + uint8_t nlog[MAX_ZONES]; /* log blocks found: 0, 1 or 2 */ + uint16_t newest[MAX_ZONES]; + int16_t zbase[MAX_ZONES]; + int16_t zoffset[MAX_ZONES]; + int16_t zcand[MAX_ZONES][2]; /* one-log zones: offset candidates */ + uint16_t z; + int base_hi = -1, sys_zone = -1, i, confirm = 0, disagree = 0; + int found = -1; + bool usable = false; + + for (z = 0; z < nzones; z++) + { + int votes; + + zbase[z] = -1; + zoffset[z] = -1; + zcand[z][0] = zcand[z][1] = -1; + + nlog[z] = (uint8_t)probe_find_log((uint16_t)(z << 8), &log_a[z], + &log_b[z], &newest[z]); + if (nlog[z] == 0) + { + continue; + } + if (probe_read_table(z, log_a[z], newest[z], tbl) != 0) + { + nlog[z] = 0; + continue; + } + + zbase[z] = (int16_t)probe_base(tbl, &votes); + + if (nlog[z] == 2) + { + zoffset[z] = (int16_t)probe_sys_offset(tbl, log_a[z], log_b[z]); + } + else + { + /* One survivor: the primary at table[0xff - offset] or the + * mirror at table[0xfe - offset]; settled below. */ + int j, at = -1, count = 0; + + for (j = 0; j < ZONE_BLOCKS; j++) + { + if (tbl[j] == log_a[z]) + { + count++; + at = j; + } + } + if (count == 1) + { + zcand[z][0] = (int16_t)(TBL_LOG_PRIMARY - at); + zcand[z][1] = (int16_t)((at <= TBL_LOG_MIRROR) + ? TBL_LOG_MIRROR - at : -1); + } + } + + if (zbase[z] >= 0 && zbase[z] > base_hi) + { + base_hi = zbase[z]; + } + } + + /* the offset is only visible in the system zone; elsewhere it is 0 */ + for (z = 0; z < nzones; z++) + { + if (zoffset[z] > 0 && (int)z > sys_zone) + { + sys_zone = (int)z; + } + } + + if (sys_zone >= 0) + { + if (probe_read_table((uint16_t)sys_zone, log_a[sys_zone], + newest[sys_zone], tbl) == 0) + { + ftl.base_fits = (uint8_t)probe_base_search( + tbl, (uint8_t)sys_zone, (uint8_t)zoffset[sys_zone], + nzones, &found); + } + } + else + { + /* No zone shows an offset: it is 0, or the system zone is one that + * lost a log block. Try each one-log candidate; only a unique + * winner counts. */ + int wins = 0, win_z = -1, win_so = -1, win_base = -1; + + for (z = 0; z < nzones; z++) + { + for (i = 0; i < 2; i++) + { + int f = -1, so = zcand[z][i]; + + if (nlog[z] != 1 || so <= 0) + { + continue; + } + if (probe_read_table(z, log_a[z], newest[z], tbl) != 0) + { + continue; + } + if (!probe_layout_fits(tbl, so, i == 0)) + { + continue; + } + if (probe_base_search(tbl, (uint8_t)z, (uint8_t)so, + nzones, &f) == 1) + { + wins++; + win_z = z; + win_so = so; + win_base = f; + } + } + } + + if (wins == 1) + { + sys_zone = win_z; + zoffset[win_z] = (int16_t)win_so; + ftl.base_fits = 1; + found = win_base; + } + else if (wins == 0 && nlog[0] && + probe_read_table(0, log_a[0], newest[0], tbl) == 0) + { + ftl.base_fits = (uint8_t)probe_base_search(tbl, 0, 0, nzones, + &found); + } + } + + if (sys_zone < 0) + { + ftl.boot_blocks = 0; + } + else + { + ftl.boot_blocks = (uint16_t)((sys_zone << 8) + | (uint8_t)zoffset[sys_zone]); + } + + if (ftl.base_fits == 1) + { + ftl.reserve_base = (uint8_t)found; + usable = true; + } + else if (base_hi >= 0) + { + ftl.reserve_base = (uint8_t)base_hi; /* unconfirmed: read-only */ + usable = true; + } + + if (usable) + { + /* Cross-check: every zone's log blocks must sit at the top two + * entries, as shifted. A zone that lost one confirms if the + * survivor sits at either. */ + for (z = 0; z < nzones; z++) + { + uint8_t shift; + bool ok; + + if (nlog[z] == 0 + || (nlog[z] == 2 && zbase[z] < 0 && zoffset[z] < 0)) + { + continue; + } + shift = (z <= (ftl.boot_blocks >> 8)) ? (uint8_t)ftl.boot_blocks + : 0; + if (probe_read_table(z, log_a[z], newest[z], tbl) != 0) + { + continue; + } + if (nlog[z] == 2) + { + ok = tbl[TBL_LOG_PRIMARY - shift] == log_a[z] && + tbl[TBL_LOG_MIRROR - shift] == log_b[z]; + } + else + { + ok = tbl[TBL_LOG_PRIMARY - shift] == log_a[z] || + tbl[TBL_LOG_MIRROR - shift] == log_a[z]; + } + + if (ok) + { + confirm++; + } + else + { + disagree++; + } + } + + /* an offset of 0 is only measured if the system zone itself voted */ + if (disagree || confirm == 0 || ftl.base_fits != 1 || + (sys_zone < 0 && zoffset[0] < 0)) + { + ftl.confidence = FTL_A_CONF_ASSUMED; + } + else + { + ftl.confidence = FTL_A_CONF_CONFIRMED; + set_write_protect(ftl.config.read_only); + } + } + + return usable; +} + +/* Recognise the layout. Returns true if the device is usable - read-only + * unless the layout was confirmed (ftl.confidence). */ +static bool probe_layout(void) +{ + uint16_t nzones = (uint16_t)(ftl.geo->total_blocks >> 8); + bool usable = false; + + ftl.confidence = FTL_A_CONF_NONE; + set_write_protect(true); + ftl.base_fits = 0; + + if (nzones > 0 && nzones <= MAX_ZONES) + { + usable = probe_zones(nzones); + } + return usable; +} + +/* ---- mounting ---- */ + +/* Find the newest table version in log block `sec`: the last programmed + * page, by binary search on the "programmed" metadata byte. A page whose + * neighbour below is programmed but unreadable is taken as cut short, and + * the search settles one lower. Returns the page count, 0 if nothing was + * found. */ +static uint32_t log_versions(uint32_t sec, uint8_t *buf) +{ + uint32_t pages = sec_per_block() / sec_per_page_raw(); + uint32_t lo = 0, hi = pages - 1, mid, count = 0; + bool done = false; + + while (lo <= hi && !done) + { + mid = (lo + hi) / 2; + flash_read(sec + mid * sec_per_page_raw(), NULL, buf, 1); + + if (buf[META_PROGRAMMED] != META_ERASED) + { + if (mid == pages - 1) + { + count = mid + 1; + done = true; + } + else + { + lo = mid + 1; + } + } + else if (mid == 0) + { + count = 1; + done = true; + } + else + { + uint32_t below = mid - 1; + int rc; + + hi = below; + rc = flash_read(sec + below * sec_per_page_raw(), NULL, buf, 1); + if (buf[META_PROGRAMMED] != META_ERASED) + { + count = (rc == 0) ? mid : mid - 1; + done = true; + } + } + } + return count; +} + +/* Load every zone's log position, repairing log mirrors that disagree, then + * the device state from the system zone's table and the exchange record. + * Runs after probe_layout(). */ +static enum ftl_a_error mount_scan(void) +{ + static uint8_t buf[TABLE_SIZE]; + enum ftl_a_error err = FTL_A_OK; + uint8_t off_primary = (uint8_t)(TBL_LOG_PRIMARY - system_offset()); + uint8_t off_mirror = (uint8_t)(TBL_LOG_MIRROR - system_offset()); + uint32_t zone_blk; + uint32_t addr; + int i; + + for (zone_blk = 0; zone_blk < ftl.geo->total_blocks && err == FTL_A_OK; + zone_blk += ZONE_BLOCKS) + { + uint8_t log_pos[2] = { 0xff, 0xff }; + uint8_t log_page[2] = { 0xff, 0xff }; + uint8_t found = 0; + int pos = 0xff; + uint16_t zone = (uint16_t)(zone_blk >> 8); + struct zone *z; + + if (zone_blk + ZONE_BLOCKS <= ftl.boot_blocks) + { + continue; /* all boot area */ + } + + /* the log blocks, from the top; positions 0-2 are never searched */ + while (pos > 2) + { + uint32_t sec = block_sector((uint16_t)(zone_blk + pos)); + + if (!block_is_bad(sec)) + { + flash_read(sec, NULL, buf, 1); + if (buf[META_TYPE] == META_TYPE_LOG) + { + log_pos[found] = (uint8_t)pos; + log_page[found] = (uint8_t)(log_versions(sec, buf) - 1); + found++; + if (found >= 2) + { + break; + } + } + } + pos--; + } + + if (pos <= 2 && found == 0) + { + err = FTL_A_ERR_NO_LOG_BLOCK; + continue; + } + + z = &ftl.zones[zone]; + z->log_block = log_pos[0]; + + if (log_page[0] == log_page[1]) + { + z->log_page = log_page[0]; + continue; + } + + /* The two logs disagree: a power loss during an append or a + * recycle. Rewrite both from the newest version. With one log left, + * where the two belong comes from that table. */ + if (log_pos[1] == 0xff) + { + addr = block_sector((uint16_t)(zone_blk + log_pos[0])) + + log_page[0] * sec_per_page_raw(); + } + else + { + addr = block_sector((uint16_t)(zone_blk + log_pos[1])) + + log_page[1] * sec_per_page_raw(); + } + flash_read(addr, buf, NULL, 1); + + if (log_pos[1] == 0xff) + { + if (zone <= system_zone()) + { + log_pos[0] = buf[off_primary]; + log_pos[1] = buf[off_mirror]; + } + else + { + log_pos[0] = buf[TBL_LOG_PRIMARY]; + log_pos[1] = buf[TBL_LOG_MIRROR]; + } + z->log_block = log_pos[0]; + } + + /* A read-only mount cannot repair, and half a repair is worse than + * none - a refused erase leaves log_page pointing at a page never + * written, and every lookup in the zone reads garbage. */ + if (ftl.write_protect) + { + err = FTL_A_ERR_NEEDS_REPAIR; + continue; + } + + addr = block_sector((uint16_t)(zone_blk + log_pos[0])); + if (flash_erase(addr) != 0) + { + addr = relocate_block(addr, NULL, 0); + } + flash_program(addr, buf, log_block_meta, 1); + + addr = block_sector((uint16_t)(zone_blk + log_pos[1])); + if (flash_erase(addr) != 0) + { + addr = relocate_block(addr, NULL, 0); + } + flash_program(addr, buf, log_block_meta, 1); + + z->log_page = 0; + } + + if (err == FTL_A_OK) + { + uint32_t idx = REPLACEMENT_SLOTS - 1; + uint32_t nzones = ftl.geo->total_blocks >> 8; + uint32_t z; + + flash_read(system_table_sector(), buf, NULL, 1); + ftl.format_flag = buf[TBL_FORMAT_FLAG]; + + /* The exchange record's newest page is searched for from page 9 + * down, by its byte 0 - which relocate_block() never writes; so it + * always settles on page 0, and the replaced-block list is always + * the one formatting left, i.e. empty. As in the original firmware: + * a replaced block is found bad again rather than remembered. */ + addr = block_sector((uint16_t)(buf[xrec_index()] + + (system_zone() << 8))); + for (;;) + { + flash_read(addr + idx * sec_per_page_raw(), buf, NULL, 1); + if (buf[0] != META_ERASED || idx == 0) + { + break; + } + idx--; + } + + for (i = 0; i < REPLACEMENT_SLOTS; i++) + { + ftl.replaced[i] = (uint16_t)(buf[XREC_REPLACED + i * 2] | + (buf[XREC_REPLACED + i * 2 + 1] << 8)); + } + + for (i = 0; i < MAX_ZONES; i++) + { + ftl.zones[i].block_count = buf[i]; + } + + ftl.logical_sectors = 0; + for (z = 0; z < nzones && z < MAX_ZONES; z++) + { + ftl.logical_sectors += ftl.zones[z].block_count; + } + ftl.logical_sectors *= sec_per_block(); + + if (ftl.logical_sectors == 0) + { + err = FTL_A_ERR_ZERO_CAPACITY; + } + else + { + /* A count of 0xff is an erased page, not a zone: every zone + * keeps at least its log blocks and pool out of its 256. */ + for (z = 0; z < nzones && z < MAX_ZONES; z++) + { + if (ftl.zones[z].block_count == 0xff) + { + err = FTL_A_ERR_COUNTS_ERASED; + } + } + } + } + + return err; +} + +/* ---- reading and writing ---- */ + +static int read_sectors(int volume, uint32_t lba, uint8_t *p, uint16_t count) +{ + int ret = 0; + + if (volume == FTL_A_VOL_USER) + { + lba += volume_sectors(FTL_A_VOL_SYS); + } + + while (count) + { + uint32_t addr = map_current(lba); + struct write_cache *hit = NULL; + uint16_t len; + int i; + + /* a page being assembled holds the newest copy of what it holds */ + for (i = 0; i < WRITE_CACHE_PAGES; i++) + { + struct write_cache *c = &ftl.cache[i]; + + uint32_t held = c->next_lba & ~(uint32_t)(sec_per_page() - 1); + + if (c->valid && held <= lba && c->next_lba > lba) + { + hit = c; + break; + } + } + + if (hit == NULL) + { + uint16_t in_page = (uint16_t)(lba % sec_per_page_raw()); + + len = sec_per_page_raw() - in_page; + if (len > count) + { + len = count; + } + if (flash_read(addr, p, NULL, len) != 0) + { + ret = 1; + } + } + else + { + uint16_t in_page = (uint16_t)(lba % sec_per_page()); + + len = (uint16_t)(hit->next_lba % sec_per_page() - in_page); + if (len > count) + { + len = count; + } + memcpy(p, hit->buf + in_page * FLASH_SECTOR_SIZE, + (size_t)len * FLASH_SECTOR_SIZE); + } + + p += (uint32_t)len * FLASH_SECTOR_SIZE; + count -= len; + lba += len; + } + + return ret; +} + +/* Write a cached page out: complete it from the block being replaced, and + * program it at its exchange slot's cursor. A page that holds no sector yet + * is dropped. */ +static void cache_flush(struct write_cache *c) +{ + if (c->valid) + { + uint32_t lba = c->next_lba; + uint32_t addr = map_committed(lba); + uint8_t spp = sec_per_page(), sppr = sec_per_page_raw(); + uint16_t in_page = (uint16_t)(lba & (spp - 1)); + + if (in_page != 0) + { + uint32_t page_base = (lba % sec_per_block()) & ~(uint32_t)(spp - 1); + struct exch_slot *s = c->slot; + + /* the slot may have moved on since; take the current one. The + * original firmware does not store it back - neither does this, + * the page is done with it. */ + if ((addr & ~(uint32_t)(spp - 1)) != s->cursor) + { + s = exch_for_write(lba & ~(uint32_t)(spp - 1)); + } + + if (in_page <= sppr && ftl.geo->planes > 1) + { + flash_read(s->src + page_base + in_page, + c->buf + (uint32_t)in_page * FLASH_SECTOR_SIZE, NULL, + (uint8_t)(sppr - in_page)); + flash_read(s->src + page_base + sppr, + c->buf + (uint32_t)sppr * FLASH_SECTOR_SIZE, + NULL, sppr); + } + else + { + flash_read(s->src + page_base + in_page, + c->buf + (uint32_t)in_page * FLASH_SECTOR_SIZE, NULL, + (uint8_t)(spp - in_page)); + } + + flash_program_page(s->cursor, c->buf, NULL); + s->cursor += spp; + } + c->valid = false; + } +} + +static void cache_flush_all(void) +{ + int i; + + for (i = 0; i < WRITE_CACHE_PAGES; i++) + { + cache_flush(&ftl.cache[i]); + } +} + +/* Stage sectors into a page buffer; a completed page goes to its exchange + * slot. A buffer is taken - in this order - from one already holding the + * start of the same page (flushed first unless it ends right where this + * write begins), from the free ones, or from the least recently used. */ +static int write_sectors(int volume, uint32_t lba, const uint8_t *p, + uint16_t count) +{ + struct write_cache *slot = NULL; + uint32_t page_mask = sec_per_page() - 1; + uint32_t page_lba; + int i; + + if (volume == FTL_A_VOL_USER) + { + lba += volume_sectors(FTL_A_VOL_SYS); + } + + /* The original firmware resolves the first sector here, before anything + * else - it wants the chip it is on. Kept: it loads the zone's table, + * and the flash access of this driver was validated operation for + * operation against the original's. */ + map_current(lba); + + page_lba = lba & ~page_mask; + for (i = 0; i < WRITE_CACHE_PAGES && slot == NULL; i++) + { + struct write_cache *c = &ftl.cache[i]; + + if (c->valid && (c->next_lba & ~page_mask) == page_lba) + { + if (c->next_lba != lba) + { + cache_flush(c); + } + slot = c; + } + } + + for (i = 0; i < WRITE_CACHE_PAGES && slot == NULL; i++) + { + if (!ftl.cache[i].valid) + { + slot = &ftl.cache[i]; + } + } + + for (i = 0; i < WRITE_CACHE_PAGES && slot == NULL; i++) + { + if (ftl.cache[i].age == 0) + { + cache_flush(&ftl.cache[i]); + slot = &ftl.cache[i]; + } + } + + /* The ages form a permutation, so the least recently used buffer - age + * 0 - always exists. They have to be updated whichever way the buffer + * was chosen for that to hold. */ + for (i = 0; i < WRITE_CACHE_PAGES; i++) + { + if (ftl.cache[i].age > slot->age) + { + ftl.cache[i].age--; + } + } + slot->age = WRITE_CACHE_PAGES - 1; + + /* A page further on in this write may still sit, part written, in + * another buffer - left there by an earlier write that ended inside it. + * This write reaches that page through its own buffer, so the old one + * goes to flash first: otherwise two buffers hold the page, reads find + * the older, and its later flush programs stale data over the newer. + * The original firmware lacks this and loses data that way. */ + for (i = 0; i < WRITE_CACHE_PAGES; i++) + { + struct write_cache *c = &ftl.cache[i]; + uint32_t page = c->next_lba & ~page_mask; + + if (c != slot && c->valid && page >= page_lba && page < lba + count) + { + cache_flush(c); + } + } + + while (count) + { + uint32_t in_block = lba % sec_per_block(); + struct exch_slot *s = exch_for_write(lba & ~page_mask); + + slot->slot = s; + + if (!slot->valid) + { + /* start the page with what precedes this sector in it */ + uint16_t in_page = (uint16_t)(lba & page_mask); + uint8_t sppr = sec_per_page_raw(); + + if (in_page <= sppr) + { + flash_read(s->src + (in_block & ~page_mask), slot->buf, NULL, + (uint8_t)in_page); + } + else + { + uint32_t page_base = (s->src + in_block) & ~page_mask; + + flash_read(page_base, slot->buf, NULL, sppr); + flash_read(page_base + sppr, + slot->buf + (uint32_t)sppr * FLASH_SECTOR_SIZE, + NULL, (uint8_t)(in_page - sppr)); + } + slot->valid = true; + } + + memcpy(slot->buf + (lba & page_mask) * FLASH_SECTOR_SIZE, p, + FLASH_SECTOR_SIZE); + + if ((page_mask & ~lba) == 0) + { + /* the last sector of the page */ + slot->valid = false; + flash_program_page(s->cursor, slot->buf, NULL); + s->cursor += sec_per_page(); + if (s->cursor == s->end) + { + exch_close(s); + } + } + + p += FLASH_SECTOR_SIZE; + count--; + lba++; + slot->next_lba = lba; + } + + return 0; +} + +/* ---- interface ---- */ + +enum ftl_a_error ftl_a_mount(const struct ftl_a_config *config) +{ + enum ftl_a_error err; + + ftl.geo = flash_get_geometry(); + ftl.config = *config; + ftl.mounted = false; + + reset_state(); + + if (!probe_layout()) + { + err = FTL_A_ERR_LAYOUT; + } + else + { + /* the flash layer keeps writes out of the boot area, as the + * original firmware does - all writes, if the area is implausibly + * small */ + flash_set_boot_area(ftl.boot_blocks < 3 ? UINT32_MAX : + (uint32_t)ftl.boot_blocks * sec_per_block()); + + err = mount_scan(); + } + + if (err == FTL_A_OK && ftl.config.sys_sectors >= ftl.geo->total_sectors) + { + err = FTL_A_ERR_SYS_TOO_BIG; + } + + if (err == FTL_A_OK && ftl.format_flag != FORMAT_FLAG) + { + /* Another generation's flag, when the caller knows it. The older + * rk2705 NAND bootloader formats with 1; its write logic is the same + * as the flag-2 generation's, but writing such media stays the + * caller's decision. */ + if (ftl.config.alt_format_flag == 0 || + ftl.format_flag != ftl.config.alt_format_flag) + { + err = FTL_A_ERR_FORMAT_FLAG; + } + else if (!ftl.config.alt_format_writable) + { + set_write_protect(true); + } + } + + if (err == FTL_A_OK) + { + /* Recovery writes, and a wrong layout would make it relocate data + * into the wrong blocks: only on a confirmed one. Leaving an + * interrupted write unresolved is the safe side. */ + if (!ftl.write_protect) + { + recover_interrupted(); + } + ftl.mounted = true; + } + + ftl.error = err; + return err; +} + +void ftl_a_get_status(struct ftl_a_status *status) +{ + status->error = ftl.error; + status->confidence = ftl.confidence; + status->writable = !ftl.write_protect; + status->reserve_base = ftl.reserve_base; + status->sys_zone = system_zone(); + status->sys_offset = system_offset(); + status->base_fits = ftl.base_fits; + status->format_flag = ftl.format_flag; +} + +uint32_t ftl_a_capacity(int volume) +{ + uint32_t sectors = 0; + + if (ftl.mounted) + { + sectors = volume_sectors(volume); + } + return sectors; +} + +/* Requests are split in pieces the page-sized internals count in 16 bits. */ +#define MAX_REQUEST 0x8000 + +int ftl_a_read(int volume, uint32_t sector, void *buf, uint32_t count) +{ + uint8_t *p = buf; + int ret = 0; + + if (!ftl.mounted || (volume != FTL_A_VOL_SYS && volume != FTL_A_VOL_USER) || + sector + count > volume_sectors(volume)) + { + ret = 1; + } + + while (ret == 0 && count > 0) + { + uint16_t n = (count > MAX_REQUEST) ? MAX_REQUEST : (uint16_t)count; + + ret = read_sectors(volume, sector, p, n); + sector += n; + count -= n; + p += (size_t)n * FLASH_SECTOR_SIZE; + } + return ret; +} + +int ftl_a_write(int volume, uint32_t sector, const void *buf, uint32_t count) +{ + const uint8_t *p = buf; + int ret = 0; + + if (!ftl.mounted || ftl.write_protect || + (volume != FTL_A_VOL_SYS && volume != FTL_A_VOL_USER) || + sector + count > volume_sectors(volume)) + { + ret = 1; + } + + while (ret == 0 && count > 0) + { + uint16_t n = (count > MAX_REQUEST) ? MAX_REQUEST : (uint16_t)count; + + ret = write_sectors(volume, sector, p, n); + sector += n; + count -= n; + p += (size_t)n * FLASH_SECTOR_SIZE; + } + return ret; +} + +void ftl_a_sync(void) +{ + if (ftl.mounted && !ftl.write_protect) + { + cache_flush_all(); + } +} diff --git a/firmware/target/arm/rk27xx/ftl-scheme-a.h b/firmware/target/arm/rk27xx/ftl-scheme-a.h new file mode 100644 index 0000000000..1ec9e916c2 --- /dev/null +++ b/firmware/target/arm/rk27xx/ftl-scheme-a.h @@ -0,0 +1,99 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* The "Scheme A" flash translation layer of rk27xx devices - the on-flash + * format the original firmware of many rk2705/rk2706 players uses, read and + * written compatibly so that the original firmware keeps working on the same + * media. See ftl-scheme-a.c for how it works. */ + +#ifndef __FTL_SCHEME_A_H__ +#define __FTL_SCHEME_A_H__ + +#include +#include + +/* The two volumes. SYS holds the original firmware and its resources; USER + * is the music storage. */ +#define FTL_A_VOL_SYS 0 +#define FTL_A_VOL_USER 1 + +struct ftl_a_config +{ + /* Size of the SYS volume. The one layout value the FTL cannot read from + * its own tables: it is recorded in the boot area's ID block. */ + uint32_t sys_sectors; + /* Never write, not even the repairs a mount normally makes. */ + bool read_only; + /* A format flag accepted besides the standard 2 (0 = none), and whether + * media carrying it may be written. See ftl_a_mount(). */ + uint8_t alt_format_flag; + bool alt_format_writable; +}; + +/* Why a mount failed */ +enum ftl_a_error +{ + FTL_A_OK = 0, + FTL_A_ERR_LAYOUT, /* the zone layout could not be recognised */ + FTL_A_ERR_NO_LOG_BLOCK, /* a zone has no remap log: damaged */ + FTL_A_ERR_ZERO_CAPACITY, /* the block counts never loaded */ + FTL_A_ERR_COUNTS_ERASED, /* ... or were read from an erased page */ + FTL_A_ERR_SYS_TOO_BIG, /* SYS larger than the chip */ + FTL_A_ERR_FORMAT_FLAG, /* not formatted, or an unknown generation */ + FTL_A_ERR_NEEDS_REPAIR, /* a log needs rewriting; not while read-only */ +}; + +/* How sure the mount is of the layout it recognised */ +enum ftl_a_confidence +{ + FTL_A_CONF_NONE = 0, + FTL_A_CONF_CONFIRMED, /* derived and cross-checked: writable */ + FTL_A_CONF_ASSUMED, /* partly assumed: read-only */ +}; + +struct ftl_a_status +{ + enum ftl_a_error error; + enum ftl_a_confidence confidence; + bool writable; + uint8_t reserve_base; /* first reserve-pool entry of a zone table */ + uint8_t sys_zone; /* zone holding the boot area */ + uint8_t sys_offset; /* boot-area blocks at the start of it */ + uint8_t base_fits; /* reserve bases that fit the media; 1 = sure */ + uint8_t format_flag; /* as read from the media */ +}; + +/* Mount the media. Returns FTL_A_OK or the reason it failed. Even a + * successful mount may be read-only - see ftl_a_get_status(). */ +enum ftl_a_error ftl_a_mount(const struct ftl_a_config *config); +void ftl_a_get_status(struct ftl_a_status *status); + +/* Sectors on a volume; 0 when not mounted */ +uint32_t ftl_a_capacity(int volume); + +/* Return 0, or non-zero if the request was refused or a sector could not be + * read reliably. Sectors are 512 bytes. */ +int ftl_a_read(int volume, uint32_t sector, void *buf, uint32_t count); +int ftl_a_write(int volume, uint32_t sector, const void *buf, uint32_t count); + +/* Write out everything still held in RAM */ +void ftl_a_sync(void); + +#endif /* __FTL_SCHEME_A_H__ */ diff --git a/firmware/target/arm/rk27xx/ftl-target.h b/firmware/target/arm/rk27xx/ftl-target.h index e432ad1b40..d2dc6f86b6 100644 --- a/firmware/target/arm/rk27xx/ftl-target.h +++ b/firmware/target/arm/rk27xx/ftl-target.h @@ -24,21 +24,26 @@ #include "config.h" #include "inttypes.h" -#ifdef BOOTLOADER -/* Bootloaders don't need write access */ -#define FTL_READONLY +/* The drives the NAND presents: USER, and SYS only when the target exposes + * it - see HAVE_RK27XX_NAND_SYS in the target config. */ +#ifdef HAVE_RK27XX_NAND_SYS +#define FTL_DRIVE_SYS 0 +#define FTL_DRIVE_USER 1 +#define FTL_NUM_DRIVES 2 +#else +#define FTL_DRIVE_USER 0 +#define FTL_NUM_DRIVES 1 #endif -/* Pointer to an info structure regarding the flash type used */ -extern const struct nand_device_info_type* ftl_nand_type; - -/* Number of banks we detected a chip on */ -extern uint32_t ftl_banks; - uint32_t ftl_init(void); -uint32_t ftl_read(uint32_t sector, uint32_t count, void* buffer); -uint32_t ftl_write(uint32_t sector, uint32_t count, const void* buffer); +uint32_t ftl_read(int drive, uint32_t sector, uint32_t count, void* buffer); +uint32_t ftl_write(int drive, uint32_t sector, uint32_t count, + const void* buffer); uint32_t ftl_sync(void); +/* Usable sectors on a logical disk, 0 if not mounted. Comes from the FTL's + * own tables, not from raw block geometry - see ftl-rk27xx.c. */ +uint32_t ftl_get_sectors(int drive); + #endif From 0db035d2314daab93e049631ad481a51d4718475 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 23 Sep 2026 19:03:59 +0200 Subject: [PATCH 293/408] rk27generic: expose the NAND as a drive The NAND holds two volumes, SYS and USER; the split is recorded in the boot area, so they are separate drives rather than partitions of one. SYS is hidden: nothing in it is the user's, and deleting or overfilling it stops the device booting. HAVE_RK27XX_NAND_SYS, documented in the config and off, brings it back as a drive of its own. storage.c numbers drives by driver, SD first: default HAVE_RK27XX_NAND_SYS drive 0 SD SD drive 1 NAND USER NAND SYS drive 2 NAND USER NUM_DRIVES was 1, which was already wrong for a target whose CONFIG_STORAGE names both SD and NAND. Co-Authored-By: Claude Opus 5.5 Change-Id: Ic68921838faef0ff6fd296d43e86ec3a10a091c5 --- firmware/export/config/rk27generic.h | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/firmware/export/config/rk27generic.h b/firmware/export/config/rk27generic.h index c31efd0a7e..8c1eaa3975 100644 --- a/firmware/export/config/rk27generic.h +++ b/firmware/export/config/rk27generic.h @@ -56,7 +56,6 @@ #define HAVE_FLASH_STORAGE #define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) -#define NUM_DRIVES 1 /* NAND doesn't work yet */ #define CONFIG_NAND NAND_RK27XX #define HAVE_SW_TONE_CONTROLS @@ -64,6 +63,31 @@ /* commented for now */ /* #define HAVE_HOTSWAP */ +/* The NAND holds two volumes (ftl-scheme-a.c): SYS, ~94 MB, where the + * original firmware keeps itself - on a Rockbox device that includes the + * BASE.RKW that chainloads the Rockbox bootloader - and USER, the rest of the + * chip. The split is recorded in the boot area, so they are separate drives + * rather than partitions of one. + * + * SYS is hidden: nothing in it is the user's, and deleting or overfilling it + * stops the device booting. Define HAVE_RK27XX_NAND_SYS - here, or in a + * purpose build - to present it as a drive of its own, readable and, with + * FTL_ALLOW_WRITE, writable like USER. + * + * storage.c numbers drives by driver, SD first: + * + * default HAVE_RK27XX_NAND_SYS + * drive 0 SD SD + * drive 1 NAND USER NAND SYS + * drive 2 NAND USER + */ +/* #define HAVE_RK27XX_NAND_SYS */ + +#ifdef HAVE_RK27XX_NAND_SYS +#define NUM_DRIVES 3 +#else +#define NUM_DRIVES 2 +#endif #define SECTOR_SIZE 512 /* for small(ish) SD cards */ From 6bf3eaff4161f9fe071b1356823bb8f6fba29c19 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 09:26:56 +0200 Subject: [PATCH 294/408] rk27xx: let an FTL keep its own data in metadata byte 1 The flash layer writes 0x00 into metadata byte 1 of every page it programs, which is how Scheme A tells a programmed page from an erased one. Scheme B keeps a 16-bit field in bytes 0-1 of every sector - its block tags, versions and block numbers - so it needs the byte as written. Add flash_set_meta_passthrough() to turn the forcing off, and flash_copy_meta(), a copy that either keeps each sector's own metadata or programs a page of it given by the caller: Scheme B's copy stamps one header on every page it moves. Co-Authored-By: Claude Opus 5.5 Change-Id: I49b2c24d812284af2c50d776f0942b5b1b989c98 --- firmware/target/arm/rk27xx/flash-rk27xx.c | 56 +++++++++++++++++++++-- firmware/target/arm/rk27xx/flash-rk27xx.h | 18 ++++++-- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.c b/firmware/target/arm/rk27xx/flash-rk27xx.c index c419efcc9f..355d9268ea 100644 --- a/firmware/target/arm/rk27xx/flash-rk27xx.c +++ b/firmware/target/arm/rk27xx/flash-rk27xx.c @@ -76,6 +76,7 @@ static struct flash_geometry geo; static bool ready; static bool writable; +static bool meta_passthrough; static uint32_t boot_area = UINT32_MAX; static struct flash_stats stats; @@ -129,6 +130,11 @@ void flash_set_boot_area(uint32_t sectors) boot_area = sectors; } +void flash_set_meta_passthrough(bool on) +{ + meta_passthrough = on; +} + void flash_get_stats(struct flash_stats *out) { *out = stats; @@ -393,7 +399,10 @@ static int prog_raw_run(uint32_t row, uint32_t first, uint32_t n, { memcpy(spare, meta + i * FLASH_META_SIZE, FLASH_META_SIZE); } - spare[META_PROGRAMMED] = 0x00; + if (!meta_passthrough) + { + spare[META_PROGRAMMED] = 0x00; + } /* a slot is reused every four sectors: its last transfer must be * done */ @@ -547,14 +556,24 @@ int flash_erase(uint32_t sec) return ret; } +/* What a copy programs as the destination's metadata */ +enum copy_meta +{ + COPY_META_FRESH, /* as flash_program() with meta NULL */ + COPY_META_KEEP, /* each sector's own */ + COPY_META_PAGE, /* the caller's, by position in the page */ +}; + /* Copy through the ECC engine, never with the chip's internal data move: on * this MLC part moved pages accumulate bit errors, and the data area's * t=8 is already below the chip's 12-bit minimum. A source sector that * fails ECC is copied as read, and counted. */ -int flash_copy(uint32_t src, uint32_t dst, unsigned n) +static int copy_sectors(uint32_t src, uint32_t dst, unsigned n, + enum copy_meta how, const uint8_t *page_meta) { static uint8_t buf[FLASH_MAX_SEC_PER_PAGE * FLASH_SECTOR_SIZE] __attribute__((aligned(4))); + static uint8_t meta[FLASH_MAX_SEC_PER_PAGE * FLASH_META_SIZE]; uint32_t k = 0; int ret = 0; @@ -599,14 +618,30 @@ int flash_copy(uint32_t src, uint32_t dst, unsigned n) } if (read_raw_run(raw / geo.sec_per_page_raw, first, run, - buf + (size_t)i * FLASH_SECTOR_SIZE, NULL)) + buf + (size_t)i * FLASH_SECTOR_SIZE, + how == COPY_META_KEEP ? + meta + (size_t)i * FLASH_META_SIZE : NULL)) { stats.copy_uncorrectable++; } i += run; } - if (ret == 0 && flash_program(dst + k, buf, NULL, len)) + if (how == COPY_META_PAGE) + { + for (i = 0; i < len; i++) + { + uint32_t at = (dst + k + i) % geo.sec_per_page; + + memcpy(meta + i * FLASH_META_SIZE, + page_meta + at * FLASH_META_SIZE, + FLASH_META_SIZE); + } + } + + if (ret == 0 + && flash_program(dst + k, buf, + how == COPY_META_FRESH ? NULL : meta, len)) { ret = 1; } @@ -615,3 +650,16 @@ int flash_copy(uint32_t src, uint32_t dst, unsigned n) } return ret; } + +int flash_copy(uint32_t src, uint32_t dst, unsigned n) +{ + return copy_sectors(src, dst, n, COPY_META_FRESH, NULL); +} + +int flash_copy_meta(uint32_t src, uint32_t dst, unsigned n, + const void *page_meta) +{ + return copy_sectors(src, dst, n, + page_meta ? COPY_META_PAGE : COPY_META_KEEP, + page_meta); +} diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.h b/firmware/target/arm/rk27xx/flash-rk27xx.h index 952d537ffc..fe6074069f 100644 --- a/firmware/target/arm/rk27xx/flash-rk27xx.h +++ b/firmware/target/arm/rk27xx/flash-rk27xx.h @@ -33,8 +33,10 @@ * METADATA: every sector carries 16 spare bytes. The last 13 are the BCH * code and belong to the hardware; the first three are the FTL's, and are * what the meta arguments below carry - FLASH_META_SIZE bytes per sector. - * Byte 1 is special: this layer writes 0x00 there on every program, so that - * the FTL can tell a programmed page from an erased one whatever it wrote. + * Byte 1 is special: by default this layer writes 0x00 there on every + * program, so that the FTL can tell a programmed page from an erased one + * whatever it wrote. An FTL that keeps its own data in byte 1 turns that off + * with flash_set_meta_passthrough(). * * Only the first chip is supported; every device the FTL has been validated * on has one. */ @@ -86,7 +88,8 @@ const struct flash_geometry *flash_get_geometry(void); int flash_read(uint32_t sec, void *data, void *meta, unsigned n); /* Program n sectors from sec, which must be erased. data NULL programs - * 0xff; meta NULL programs {0xff, 0x00, 0xff}. Returns 0 or 1 on failure. */ + * 0xff; meta NULL programs {0xff, 0x00, 0xff} - {0xff, 0xff, 0xff} with + * passthrough on. Returns 0 or 1 on failure. */ int flash_program(uint32_t sec, const void *data, const void *meta, unsigned n); /* Program the whole page (every plane) that contains sec. */ @@ -100,6 +103,15 @@ int flash_erase(uint32_t sec); * gets fresh metadata {0xff, 0x00, 0xff}. */ int flash_copy(uint32_t src, uint32_t dst, unsigned n); +/* As flash_copy(), but with chosen metadata: page_meta NULL keeps each + * sector's own; otherwise sector k of every destination page is programmed + * with page_meta[k] (sec_per_page * FLASH_META_SIZE bytes). */ +int flash_copy_meta(uint32_t src, uint32_t dst, unsigned n, + const void *page_meta); + +/* Program metadata byte 1 as given rather than 0x00. */ +void flash_set_meta_passthrough(bool on); + /* Read one PHYSICAL sector, bypassing the super-block view: for the boot * area, which the boot ROM addresses physically. */ int flash_read_raw(uint32_t raw_sec, void *data, void *meta); From efde59472e996bfc0f31602638715f103ec7f47d Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 09:28:08 +0200 Subject: [PATCH 295/408] rk27xx: make the BCH strength of the FTL area selectable The controller has a second ECC mode, BCHCTL bit 13: t=14 instead of t=8, over the same field and polynomial. Some firmware writes its FTL area in it - the Archos Vision 28 does - while every boot area seen is t=8. flash_set_ecc() selects the mode for reads and copies of the FTL area; flash_read_raw(), which reads the boot area, stays at t=8. Writing is refused in t=14 mode: a t=14 sector is a 538-byte record on the media (3 metadata and 23 parity bytes), the program path addresses 528-byte records, and programming in that mode has not been tried. Co-Authored-By: Claude Opus 5.5 Change-Id: Iaf4ce9274b72838b7719232fc49cdce0cceecbac --- firmware/target/arm/rk27xx/flash-rk27xx.c | 47 ++++++++++++++++++----- firmware/target/arm/rk27xx/flash-rk27xx.h | 8 ++++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.c b/firmware/target/arm/rk27xx/flash-rk27xx.c index 355d9268ea..f3a9ab78b1 100644 --- a/firmware/target/arm/rk27xx/flash-rk27xx.c +++ b/firmware/target/arm/rk27xx/flash-rk27xx.c @@ -77,6 +77,7 @@ static struct flash_geometry geo; static bool ready; static bool writable; static bool meta_passthrough; +static uint32_t ecc_mode; /* BCHCTL mode bits: 0 or BCH_T14 */ static uint32_t boot_area = UINT32_MAX; static struct flash_stats stats; @@ -135,6 +136,25 @@ void flash_set_meta_passthrough(bool on) meta_passthrough = on; } +int flash_set_ecc(unsigned t) +{ + int ret = 0; + + if (t == 8) + { + ecc_mode = 0; + } + else if (t == 14) + { + ecc_mode = BCH_T14; + } + else + { + ret = 1; + } + return ret; +} + void flash_get_stats(struct flash_stats *out) { *out = stats; @@ -208,8 +228,9 @@ static void send_row(uint32_t row) FLASH_ADDR(0) = (row >> 16) & 0xff; } -/* Latch raw page `row` in the chip and prepare the ECC engine to decode. */ -static void latch_page(uint32_t row) +/* Latch raw page `row` in the chip and prepare the ECC engine to decode in + * `mode` (BCHCTL mode bits). */ +static void latch_page(uint32_t row, uint32_t mode) { flash_chip_select(0); wait_flash_ready(); @@ -222,8 +243,7 @@ static void latch_page(uint32_t row) wait_flash_ready(); - /* ECC on, t=8 - right for the whole chip on the devices seen */ - BCHCTL = BCH_RST; + BCHCTL = BCH_RST | mode; } /* Transfer the next sector of the latched page through slot `slot` & 3. @@ -256,12 +276,12 @@ static uint32_t read_next_sector(uint32_t slot, uint8_t *data, uint8_t *meta) /* Read sectors [first, first + n) of raw page `row`. Returns 1 if any was * uncorrectable. */ static int read_raw_run(uint32_t row, uint32_t first, uint32_t n, - uint8_t *data, uint8_t *meta) + uint8_t *data, uint8_t *meta, uint32_t mode) { uint32_t j; int uncorrectable = 0; - latch_page(row); + latch_page(row, mode); for (j = 0; j < first + n; j++) { @@ -296,8 +316,10 @@ int flash_read_raw(uint32_t raw_sec, void *data, void *meta) if (ready && raw_sec < geo.total_sectors) { + /* the boot area is t=8 on every device seen, whatever the FTL + * area uses */ ret = read_raw_run(raw_sec / geo.sec_per_page_raw, - raw_sec % geo.sec_per_page_raw, 1, data, meta); + raw_sec % geo.sec_per_page_raw, 1, data, meta, 0); } return ret; } @@ -324,7 +346,7 @@ int flash_read(uint32_t sec, void *data, void *meta, unsigned n) if (read_raw_run(raw / geo.sec_per_page_raw, raw % geo.sec_per_page_raw, 1, d ? d + (size_t)i * FLASH_SECTOR_SIZE : NULL, - m ? m + (size_t)i * FLASH_META_SIZE : NULL)) + m ? m + (size_t)i * FLASH_META_SIZE : NULL, ecc_mode)) { ret = 1; } @@ -334,9 +356,13 @@ int flash_read(uint32_t sec, void *data, void *meta, unsigned n) /* ---- writing ---- */ +/* A write is refused in t=14 mode too: a t=14 sector is a 538-byte record + * on the media - 512 data, 3 metadata, 23 parity - where the program path + * below addresses 528-byte records, and programming in that mode has not + * been tried. */ static bool write_refused(void) { - bool refused = !ready || !writable; + bool refused = !ready || !writable || ecc_mode != 0; if (refused) { @@ -620,7 +646,8 @@ static int copy_sectors(uint32_t src, uint32_t dst, unsigned n, if (read_raw_run(raw / geo.sec_per_page_raw, first, run, buf + (size_t)i * FLASH_SECTOR_SIZE, how == COPY_META_KEEP ? - meta + (size_t)i * FLASH_META_SIZE : NULL)) + meta + (size_t)i * FLASH_META_SIZE : NULL, + ecc_mode)) { stats.copy_uncorrectable++; } diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.h b/firmware/target/arm/rk27xx/flash-rk27xx.h index fe6074069f..ed99cf7d9e 100644 --- a/firmware/target/arm/rk27xx/flash-rk27xx.h +++ b/firmware/target/arm/rk27xx/flash-rk27xx.h @@ -112,6 +112,14 @@ int flash_copy_meta(uint32_t src, uint32_t dst, unsigned n, /* Program metadata byte 1 as given rather than 0x00. */ void flash_set_meta_passthrough(bool on); +/* The BCH strength of the FTL's area: t = 8 (the default) or 14, the + * controller's BCH_T14 mode. flash_read_raw() always reads t=8, as the + * boot area is on every device seen. Writing is refused in t=14 mode: its + * sectors are 538-byte records on the media (3 + 23 spare bytes), and + * programming them has not been tried. + * Returns 0, or 1 for an unsupported t. */ +int flash_set_ecc(unsigned t); + /* Read one PHYSICAL sector, bypassing the super-block view: for the boot * area, which the boot ROM addresses physically. */ int flash_read_raw(uint32_t raw_sec, void *data, void *meta); From 16492de56934c3f32975f0b69a94bb8f4697c157 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 09:28:08 +0200 Subject: [PATCH 296/408] rk27xx: add the Scheme B flash translation layer Scheme B is the self-describing NAND format of the HiFiMAN HM-601 and similar players: every block carries its logical number and a version in its metadata, so the mapping is rebuilt by a scan at mount, and small writes go through a 16-page RAM cache journalled to flash. ftl-scheme-b.c is a reimplementation from reverse engineering. The format and the behaviour were worked out by analysing the machine code of the HM-601's NAND bootloader and of a compiled Rockchip FTL object from the rk2808 platform, which handles the same format, and checked against dumps of the media; no source code was used. Where the two binaries differ the HM-601 is followed: 16-bit versions compared across wrap, plain 0xF200/0xF100 tags, a copy that stamps one header on every page. The number of open exchange blocks is configurable - 8 on the HM-601, whose mount recovers no more. Checked by running the compiled object under qemu over a NAND simulator, side by side with this code, on a 4 GiB HM-601 dump: the same state after mount, identical reads of all 3958 logical blocks, and flash programs and erases identical one for one - over 600 random writes on each of three seeds and at every power-cut point of three sweeps, 1435 points - with 0 wrong sectors. Not built yet: no target selects it. Co-Authored-By: Claude Opus 5.5 Change-Id: Icb935ea78d716c1e51f1453fc3ab3f47f3467027 --- firmware/target/arm/rk27xx/ftl-scheme-b.c | 1780 +++++++++++++++++++++ firmware/target/arm/rk27xx/ftl-scheme-b.h | 87 + 2 files changed, 1867 insertions(+) create mode 100644 firmware/target/arm/rk27xx/ftl-scheme-b.c create mode 100644 firmware/target/arm/rk27xx/ftl-scheme-b.h diff --git a/firmware/target/arm/rk27xx/ftl-scheme-b.c b/firmware/target/arm/rk27xx/ftl-scheme-b.c new file mode 100644 index 0000000000..1a817ade18 --- /dev/null +++ b/firmware/target/arm/rk27xx/ftl-scheme-b.c @@ -0,0 +1,1780 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* THE SCHEME B FLASH TRANSLATION LAYER + * + * Later rk27xx players - the HiFiMAN HM-601 among them - keep their NAND in + * a different format from Scheme A's (ftl-scheme-a.c): one where every + * block says what it holds, so the whole mapping is rebuilt by scanning at + * every mount and there is no table to keep consistent. This file reads and + * writes that format as the original firmware does, so a device keeps + * working with its original firmware after Rockbox has written to it. + * + * It is a reimplementation from reverse engineering. The format and the + * behaviour were worked out by analysing the machine code of the HM-601's + * NAND bootloader and of a compiled Rockchip FTL object from the rk2808 + * platform, which handles the same format, and checked against dumps of + * the media; no source code was used. Where the two binaries differ, the + * HM-601's behaviour is followed and the place is marked. "The original" + * below means that observed behaviour. Where it is odd, it is reproduced on + * purpose and marked as such: the order in which blocks are used and pages + * cached is part of what is checked against it. + * + * + * BLOCKS + * + * The unit of mapping is the SUPER-BLOCK - on a two-plane chip, a physical + * block from each plane, used as one (flash-rk27xx.h) - and the logical + * space is a run of logical blocks of that size. The blocks before + * config.first_block are the boot area. Then come the FTL's system blocks, + * the last of them named by the bad-block table, then the data area. + * + * + * METADATA + * + * The original firmware sees 16 spare bytes per sector and keeps one 16-bit + * FIELD in the first two of each; of a page, the field of sector k is field + * k. rk27 media holds the first three bytes of each sector's spare, so a + * field is metadata bytes 0-1, and byte 2 is used only where the original + * stores 32-bit values. Field 0 of every page is its TAG: + * + * 0xf200 data 1 version, 2 logical block, 3 source block, + * 4 the block itself + * 0xf100 cache journal 1 journal version, 2-3 first sector of the page + * (32 bits: field 2 and byte 2 of sector 2 hold the + * low 24, field 3 the high 16), 4-5 the 32-bit mask + * of sectors the page holds + * 0xf000 bad-block table + * 0xf300 power-loss record + * 0xf800 page remap not used on the devices seen; such blocks are + * freed + * 0xffff erased + * + * A data block carries its header on every page, so the last page written + * can be found by binary search. + * + * + * THE MOUNT + * + * The bad-block table (two mirror blocks at the start of the FTL's area) + * gives the bad blocks, the logical block count, the format version and the + * system blocks. Then page 0 of every block of the data area is read: a data + * block claims its logical block, and of two claimants the newer version + * wins. The (source, destination) pair of the winner is remembered: a write + * may have been under way. A destination block that is not full is an + * EXCHANGE BLOCK still being written; it is reopened, its unwritten tail + * still to come from the source. Blocks claimed by nothing are free. + * + * Versions are 16 bits and compared allowing for wrap-around, as the HM-601 + * does; the rk2808 object compares 24 bits including the tag's low byte, + * which agrees on every device seen. + * + * + * WRITING + * + * A write to a logical block goes to an erased block - an exchange block: + * the pages before the write are copied from the old block, the new pages + * written after them, and the exchange stays open for following pages + * until it is full or its slot is needed; closing it copies the rest of the + * old block. Up to config.exch_blocks are open at once, reused least + * recently used first. The old block is then free, erased when it is next + * used. + * + * A copy puts on every page it programs one header built from the source + * block's page 0 - version + 1, the source block, the destination block - + * as the HM-601 does (the rk2808 object keeps each page's own spare). + * + * + * THE WRITE CACHE + * + * Writes of less than a page, and of single pages, go to a cache of 16 + * pages in RAM, each with a mask of the sectors it holds. Every change to a + * cached page is also written to the CACHE JOURNAL - up to 16 blocks of + * 0xf100 pages - so nothing is held only in RAM: the mount replays the + * newest 16 distinct pages of the journal into the cache. A page leaves the + * cache through an exchange block, the gaps in it read from the flash first, + * and its journal entry is then cancelled by a copy with an empty mask. + * + * + * NOT IMPLEMENTED + * + * - The low-level format: media without a bad-block table does not mount. + * - The page remap (0xf800) and the paging of the remap table to flash for + * more than 32768 logical blocks; neither is used on the devices seen. + * - Bad-block replacement after a failed program or erase, and refreshing + * blocks the ECC reports decaying. + */ + +#include +#include +#include + +#include "flash-rk27xx.h" +#include "ftl-scheme-b.h" + +/* ---- layout ---- */ + +#define MAX_BLOCKS 8192 /* super-blocks on the chip */ +#define MAX_LOGICAL_BLOCKS 8192 +#define MAX_EXCH_BLOCKS 32 /* as many as the rk2808 object keeps */ +#define CACHE_PAGES 16 +#define JOURNAL_BLOCKS 16 +#define BBT_SCAN_BLOCKS 50 /* blocks the bad-block table may be in */ +#define BBT_MAX 0x400 +#define FREE_QUEUE_MIN 0x80 +#define FREE_QUEUE_MAX 0x400 +#define RCV_PAIRS 0x400 /* (source, destination) pairs kept */ +#define RECHECK_PAGES 6 /* last pages of an exchange read back */ +#define SEEN_MAX 2048 /* journal pages a mount looks at */ +#define MIN_SEC_PER_PAGE 8 /* a journal page needs fields 0-7 */ + +#define PAGE_BYTES (FLASH_MAX_SEC_PER_PAGE * FLASH_SECTOR_SIZE) +#define PAGE_META_BYTES (FLASH_MAX_SEC_PER_PAGE * FLASH_META_SIZE) + +/* The original's write path decides between the cache and an exchange + * block on fixed sector counts, whatever the page size */ +#define CACHE_FIRST_MAX 16 /* a write up to this long keeps its first + * page in the cache */ +#define CACHE_WRITE_MAX 15 /* ... and its whole pages too */ + +/* ---- metadata ---- */ + +#define TAG_TABLE 0xf000 +#define TAG_CACHE 0xf100 +#define TAG_DATA 0xf200 +#define TAG_POWER 0xf300 +#define TAG_PAGE_REMAP 0xf800 +#define TAG_KIND 0xff00 +#define FIELD_ERASED 0xffff + +/* data page */ +#define F_TAG 0 +#define F_VER 1 +#define F_LBLOCK 2 +#define F_SRC 3 +#define F_SELF 4 +/* cache journal page */ +#define F_LBA_LO 2 +#define F_LBA_HI 3 +#define F_MASK_LO 4 +#define F_MASK_HI 5 +#define JOURNAL_FIELDS 8 +/* bad-block table page */ +#define F_LOGICAL 2 +#define F_MIRROR_A 4 +#define F_MIRROR_B 5 +#define F_POWER_BLOCK 6 +#define F_LAST_SYS_BLOCK 7 +#define BBT_FIELDS 8 +#define FORMAT_VERSION_MAJOR 2 + +/* power-loss record: three little-endian words of the page's first sector */ +#define PWR_MODE_DONE 0x55530000 +#define PWR_MODE_COPY 0x55531111 + +#define DATA_HEADER_FIELDS 5 +#define HEADER_PAD 0xff /* what the unused metadata bytes get */ + +#define NO_SLOT 0xff +#define NO_JOURNAL 0xffffffff + +/* system blocks named by the bad-block table */ +#define SYS_MIRROR_A 0 +#define SYS_MIRROR_B 1 +#define SYS_POWER 2 +#define SYS_LAST 3 +#define SYS_BLOCKS 4 + +/* ---- state ---- */ + +/* A logical block being rewritten into an exchange block */ +struct exch_block +{ + uint32_t start; /* next sector to write in the exchange block */ + uint32_t end; /* the sector after the exchange block */ + uint32_t src; /* first sector of the block being replaced */ + uint16_t ver; /* version of the exchange block's header */ + uint8_t age; /* exch_count - 1 = used most recently */ + bool open; +}; + +/* A page of the write cache */ +struct cache_page +{ + uint32_t lba; /* first sector of the page */ + uint32_t mask; /* the sectors held */ + uint8_t age; /* 15 = used most recently */ + bool valid; +}; + +/* The blocks waiting to be used, oldest first. A block is only ever in it + * once, and one queued when it is full is dropped - so a mount that finds + * more free blocks than the queue holds leaves the rest unused until the + * next mount, as the original does. */ +struct free_queue +{ + uint16_t max; + uint16_t front; + uint16_t rear; + uint16_t count; + uint16_t blk[FREE_QUEUE_MAX]; +}; + +static struct +{ + const struct flash_geometry *geo; + struct ftl_b_config config; + bool mounted; + bool writable; + enum ftl_b_error error; + + uint16_t total_blocks; + uint16_t logical_blocks; + uint16_t format_version; + uint16_t sys_block[SYS_BLOCKS]; + uint16_t bbt_page; /* sector of the newest table page, in block */ + uint16_t bad_count; + uint16_t bad[BBT_MAX]; + + uint16_t remap[MAX_LOGICAL_BLOCKS]; /* 0 = not mapped */ + struct free_queue free; + struct exch_block exch[MAX_EXCH_BLOCKS]; + + /* the cache journal: its blocks, oldest first, the one being written + * and the next sector in it */ + uint16_t journal_blk[JOURNAL_BLOCKS]; + int8_t journal_slot; /* -1 = no journal yet */ + uint32_t journal_page; /* NO_JOURNAL = the block is full */ + uint16_t journal_ver; + + struct cache_page cache[CACHE_PAGES]; + /* the valid cache pages by ascending LBA, NO_SLOT-terminated */ + uint8_t order[CACHE_PAGES + 1]; +} ftl; + +static uint8_t cache_buf[CACHE_PAGES][PAGE_BYTES] __attribute__((aligned(4))); + +/* One page of scratch: the bad-block table as read, the journal pages a + * mount has seen. */ +static uint8_t page_buf[PAGE_BYTES] __attribute__((aligned(4))); + +/* ---- geometry helpers ---- */ + +static uint8_t sec_per_page(void) +{ + return ftl.geo->sec_per_page; +} + +static uint16_t sec_per_block(void) +{ + return ftl.geo->sec_per_block; +} + +static uint32_t block_sector(uint16_t blk) +{ + return (uint32_t)blk * sec_per_block(); +} + +static uint16_t sector_block(uint32_t sec) +{ + return (uint16_t)(sec / sec_per_block()); +} + +/* The sectors of a page from sector `sec`: every bit up to the page size */ +static uint32_t page_mask(uint32_t n) +{ + return n >= 32 ? 0xffffffff : ((uint32_t)1 << n) - 1; +} + +/* ---- metadata helpers ---- */ + +static uint16_t field(const uint8_t *meta, unsigned k) +{ + return (uint16_t)(meta[k * FLASH_META_SIZE] + | (meta[k * FLASH_META_SIZE + 1] << 8)); +} + +static void set_field(uint8_t *meta, unsigned k, uint16_t v) +{ + meta[k * FLASH_META_SIZE] = (uint8_t)v; + meta[k * FLASH_META_SIZE + 1] = (uint8_t)(v >> 8); +} + +/* The original stores a 32-bit word in the slot: the low three bytes reach + * the media. */ +static void set_word(uint8_t *meta, unsigned k, uint32_t v) +{ + meta[k * FLASH_META_SIZE] = (uint8_t)v; + meta[k * FLASH_META_SIZE + 1] = (uint8_t)(v >> 8); + meta[k * FLASH_META_SIZE + 2] = (uint8_t)(v >> 16); +} + +/* v is a later version than `than`. The HM-601's comparison: plain, or with + * both shifted half the range, so that a wrapped version still wins. */ +static bool ver_newer(uint16_t v, uint16_t than) +{ + return than < v || (uint16_t)(than + 0x8000) < (uint16_t)(v + 0x8000); +} + +/* ---- free blocks ---- */ + +static void queue_in(uint16_t blk) +{ + struct free_queue *q = &ftl.free; + bool queued = q->count == q->max; + uint16_t i; + + for (i = 0; i < q->count && !queued; i++) + { + queued = q->blk[(q->front + i) % q->max] == blk; + } + if (!queued) + { + q->blk[q->rear] = blk; + q->count++; + q->rear = (uint16_t)((q->rear + 1) % q->max); + } +} + +/* Returns 0 when there is none: block 0 is the boot area's. */ +static uint16_t queue_out(void) +{ + struct free_queue *q = &ftl.free; + uint16_t blk = 0; + + if (q->count > 0) + { + q->count--; + blk = q->blk[q->front]; + q->front = (uint16_t)((q->front + 1) % q->max); + } + return blk; +} + +static bool block_is_bad(uint16_t blk) +{ + bool bad = false; + uint16_t i; + + for (i = 0; i < ftl.bad_count && !bad; i++) + { + bad = ftl.bad[i] == blk; + } + return bad; +} + +/* ---- mapping ---- */ + +/* Map logical block `lb` to `blk`, unless blk is 0 or past the chip. Returns + * the previous mapping; 0 for a logical block out of range. */ +static uint16_t remap_set(uint16_t lb, uint16_t blk) +{ + uint16_t old = 0; + + if (lb < ftl.logical_blocks) + { + old = ftl.remap[lb]; + if (blk != 0 && blk < ftl.total_blocks) + { + ftl.remap[lb] = blk; + } + } + return old; +} + +/* The logical block stored in the block holding sector `sec`; + * logical_blocks if none. */ +static uint16_t remap_owner(uint32_t sec) +{ + uint16_t blk = sector_block(sec); + uint16_t lb; + + for (lb = 0; lb < ftl.logical_blocks && ftl.remap[lb] != blk; lb++) + { + } + return lb; +} + +/* The sector logical sector `lba` is mapped to. A logical block never + * written is given a free block here - even to be read, as the original + * does; its content is then whatever that block holds. With `current`, a + * sector of an exchange block not yet written is found in the source block + * instead. */ +static uint32_t remap_sector(uint32_t lba, bool current) +{ + uint16_t lb = sector_block(lba); + uint16_t blk = remap_set(lb, 0); + uint32_t sec; + uint8_t i; + + if (blk == 0) + { + blk = queue_out(); + remap_set(lb, blk); + } + sec = block_sector(blk) + lba % sec_per_block(); + + for (i = 0; current && i < ftl.config.exch_blocks; i++) + { + const struct exch_block *e = &ftl.exch[i]; + + if (e->open && e->start <= sec && sec < e->end) + { + sec = e->src + sec % sec_per_block(); + break; + } + } + return sec; +} + +/* ---- copying ---- */ + +/* Copy n sectors as the HM-601 does: if the source block is a data block, + * every page programmed gets the header of the source's page 0 with the + * version advanced and the source and destination blocks filled in; any + * other block is copied with each sector's own metadata. */ +static void copy_sectors(uint32_t src, uint32_t dst, uint32_t n) +{ + uint8_t meta[PAGE_META_BYTES]; + + if (n > 0) + { + flash_read(src - src % sec_per_block(), NULL, meta, sec_per_page()); + + if (field(meta, F_TAG) == TAG_DATA) + { + set_field(meta, F_VER, (uint16_t)(field(meta, F_VER) + 1)); + set_field(meta, F_SRC, sector_block(src)); + set_field(meta, F_SELF, sector_block(dst)); + flash_copy_meta(src, dst, n, meta); + } + else + { + flash_copy_meta(src, dst, n, NULL); + } + } +} + +/* ---- exchange blocks ---- */ + +static void exch_close(struct exch_block *e) +{ + if (e->open) + { + copy_sectors(e->src + e->start % sec_per_block(), e->start, + e->end - e->start); + queue_in(sector_block(e->src)); + e->open = false; + } +} + +/* Open an exchange block for the logical block mapped at `sec`, in the slot + * of an exchange already on that block, else a free slot, else the least + * recently used. */ +static struct exch_block *exch_open(uint32_t lba, uint32_t sec) +{ + struct exch_block *e = NULL; + uint8_t meta[FLASH_META_SIZE]; + uint32_t offset = sec % sec_per_block(); + uint16_t blk; + uint8_t i; + + for (i = 0; i < ftl.config.exch_blocks && e == NULL; i++) + { + if (ftl.exch[i].open + && sector_block(ftl.exch[i].start) == sector_block(sec)) + { + e = &ftl.exch[i]; + exch_close(e); + } + } + for (i = 0; i < ftl.config.exch_blocks && e == NULL; i++) + { + if (!ftl.exch[i].open) + { + e = &ftl.exch[i]; + } + } + for (i = 0; i < ftl.config.exch_blocks && e == NULL; i++) + { + if (ftl.exch[i].age == 0) + { + e = &ftl.exch[i]; + exch_close(e); + } + } + + /* the version follows the block being replaced: field 1 of its second + * sector (HM-601: 16 bits, where the rk2808 object lets it reach 2^16) */ + flash_read(sec - offset + 1, NULL, meta, 1); + e->ver = (uint16_t)(field(meta, 0) + 1); + + blk = queue_out(); + remap_set(sector_block(lba), blk); + e->start = block_sector(blk); + e->end = e->start + sec_per_block(); + e->open = true; + flash_erase(e->start); + + e->src = sec - offset; + copy_sectors(e->src, e->start, offset); + e->start += offset; + return e; +} + +/* The exchange block to write the page at `lba` to: the one writing that + * block, if the page is not behind its cursor - pages skipped are copied + * forward - or a new one. */ +static struct exch_block *exch_for_write(uint32_t lba) +{ + uint32_t sec = remap_sector(lba, false); + struct exch_block *e = NULL; + uint8_t i; + + for (i = 0; i < ftl.config.exch_blocks && e == NULL; i++) + { + struct exch_block *x = &ftl.exch[i]; + + if (x->open && x->start <= sec && sec < x->end) + { + if (x->start < sec) + { + copy_sectors(x->src + x->start % sec_per_block(), x->start, + sec - x->start); + x->start = sec; + } + e = x; + } + } + if (e == NULL) + { + e = exch_open(lba, sec); + } + + for (i = 0; i < ftl.config.exch_blocks; i++) + { + if (e->age < ftl.exch[i].age) + { + ftl.exch[i].age--; + } + } + e->age = (uint8_t)(ftl.config.exch_blocks - 1); + return e; +} + +/* The page at `lba` is the next one an exchange block writes */ +static bool exch_at(uint32_t lba) +{ + uint32_t sec = remap_sector(lba, false); + bool at = false; + uint8_t i; + + for (i = 0; i < ftl.config.exch_blocks && !at; i++) + { + at = ftl.exch[i].open && ftl.exch[i].start == sec + && sec < ftl.exch[i].end; + } + return at; +} + +/* Write one page, at a page-aligned `lba`, through an exchange block */ +static void write_page(uint32_t lba, const void *data) +{ + struct exch_block *e = exch_for_write(lba); + uint8_t meta[PAGE_META_BYTES]; + + memset(meta, HEADER_PAD, sizeof(meta)); + set_field(meta, F_TAG, TAG_DATA); /* HM-601: no version bits in it */ + set_field(meta, F_VER, e->ver); + set_field(meta, F_LBLOCK, sector_block(lba)); + set_field(meta, F_SRC, sector_block(e->src)); + set_field(meta, F_SELF, sector_block(e->start)); + + flash_program(e->start, data, meta, sec_per_page()); + e->start += sec_per_page(); + if (e->start == e->end) + { + exch_close(e); + } +} + +/* ---- the write cache ---- */ + +static uint8_t cache_index(const struct cache_page *c) +{ + return (uint8_t)(c - ftl.cache); +} + +static uint8_t *cache_data(const struct cache_page *c) +{ + return cache_buf[cache_index(c)]; +} + +/* The valid cache page holding `lba`; CACHE_PAGES if none */ +static uint8_t cache_match(uint32_t lba) +{ + uint32_t page = lba - lba % sec_per_page(); + uint8_t i; + + for (i = 0; + i < CACHE_PAGES && !(ftl.cache[i].valid && ftl.cache[i].lba == page); + i++) + { + } + return i; +} + +static uint8_t cache_first_free(void) +{ + uint8_t i; + + for (i = 0; i < CACHE_PAGES && ftl.cache[i].valid; i++) + { + } + return i; +} + +static void journal_new_block(void) +{ + uint16_t blk = queue_out(); + + ftl.journal_blk[ftl.journal_slot] = blk; + flash_erase(block_sector(blk)); +} + +static void journal_program(const struct cache_page *c) +{ + uint8_t meta[PAGE_META_BYTES]; + + memset(meta, 0xff, sizeof(meta)); + set_word(meta, F_TAG, TAG_CACHE); /* HM-601: no version bits in it */ + set_word(meta, F_VER, ftl.journal_ver); + set_word(meta, F_LBA_LO, c->lba); + set_word(meta, F_LBA_HI, c->lba >> 16); + set_word(meta, F_MASK_LO, c->mask); + set_word(meta, F_MASK_HI, c->mask >> 16); + set_word(meta, 6, 0); + set_word(meta, 7, 0); + + /* the whole page buffer, the sectors the mask leaves out included */ + flash_program(block_sector(ftl.journal_blk[ftl.journal_slot]) + + ftl.journal_page, cache_data(c), meta, sec_per_page()); + ftl.journal_page += sec_per_page(); +} + +/* Record cache page `c` in the journal. When the journal has used all its + * blocks it starts again from a new first block, freeing the others, and + * records every cached page there - `c` among them. */ +static void cache_write(struct cache_page *c) +{ + uint8_t i; + + if (c->valid) + { + if (ftl.journal_slot < 0) + { + ftl.journal_slot = 0; + ftl.journal_page = 0; + journal_new_block(); + journal_program(c); + } + else if (ftl.journal_page < sec_per_block()) + { + journal_program(c); + } + else + { + ftl.journal_ver++; + ftl.journal_page = 0; + ftl.journal_slot = + (int8_t)((ftl.journal_slot + 1) % JOURNAL_BLOCKS); + + if (ftl.journal_slot != 0) + { + journal_new_block(); + journal_program(c); + } + else + { + for (i = 0; i < JOURNAL_BLOCKS; i++) + { + queue_in(ftl.journal_blk[i]); + ftl.journal_blk[i] = 0; + } + journal_new_block(); + + /* recursion one level deep: a fresh block has room */ + for (i = 0; i < CACHE_PAGES && ftl.order[i] != NO_SLOT; i++) + { + cache_write(&ftl.cache[ftl.order[i]]); + } + } + } + } +} + +/* Drop cache page `c`, journalling it with an empty mask */ +static void cache_invalidate(struct cache_page *c) +{ + uint8_t idx = cache_index(c); + uint8_t i, k; + + for (i = 0; i < CACHE_PAGES; i++) + { + if (ftl.order[i] == idx) + { + c->mask = 0; + cache_write(c); + c->valid = false; + for (k = i; k < CACHE_PAGES; k++) + { + ftl.order[k] = ftl.order[k + 1]; + } + break; + } + } +} + +static int read_sectors(uint32_t lba, uint8_t *buf, uint32_t count); + +/* Write cache page `c` to its block - the sectors it does not hold read + * from the flash first - and drop it. */ +static void cache_flush(struct cache_page *c) +{ + uint8_t *data = cache_data(c); + uint8_t i; + + if (c->valid) + { + for (i = 0; i < sec_per_page(); i++) + { + if (!(c->mask & ((uint32_t)1 << i))) + { + read_sectors(c->lba + i, + data + (size_t)i * FLASH_SECTOR_SIZE, 1); + } + } + write_page(c->lba, data); + cache_invalidate(c); + } +} + +/* Make room in a full cache. A run of more than three consecutive pages - + * a sequential write - is flushed, from the first page an exchange block is + * waiting for, or all but its last page. Otherwise, or if that freed no + * more than two, the least recently used page goes, with the pages that + * follow it consecutively. Returns the slot to use. */ +static uint8_t cache_evict(void) +{ + uint8_t best = 0, best_start = 0, cur = 0, cur_start = 0; + uint8_t slot = CACHE_PAGES; + bool lru = true; + uint8_t j, k, n, p; + + for (j = 0; j < CACHE_PAGES && ftl.order[j + 1] != NO_SLOT; j++) + { + if (ftl.cache[ftl.order[j]].lba + sec_per_page() + == ftl.cache[ftl.order[j + 1]].lba) + { + if (cur == 0) + { + cur_start = j; + } + cur++; + } + else + { + if (best < cur) + { + best_start = cur_start; + best = cur; + } + cur = 0; + } + } + if (best < cur) + { + best_start = cur_start; + best = cur; + } + + /* best counts the steps of the run: it holds best + 1 pages. Each page + * flushed leaves the order, the next taking its position. */ + if (best + 1 > 3) + { + for (k = 0; + k < best && !exch_at(ftl.cache[ftl.order[best_start + k]].lba); + k++) + { + } + if (k < best) + { + for (n = (uint8_t)(best - k); n > 0; n--) + { + cache_flush(&ftl.cache[ftl.order[best_start + k]]); + } + lru = best + 1 - k <= 3; + } + else + { + for (n = best; n > 0; n--) + { + cache_flush(&ftl.cache[ftl.order[best_start]]); + } + lru = false; + } + } + + if (lru) + { + slot = 0; + for (j = 1; j < CACHE_PAGES; j++) + { + if (ftl.cache[j].age < ftl.cache[slot].age) + { + slot = j; + } + } + for (p = 0; p < CACHE_PAGES && ftl.order[p] != slot; p++) + { + } + while (p < CACHE_PAGES && ftl.order[p] != NO_SLOT) + { + uint8_t this = ftl.order[p], next = ftl.order[p + 1]; + bool chained = next != NO_SLOT && + ftl.cache[this].lba + sec_per_page() == ftl.cache[next].lba; + + cache_flush(&ftl.cache[this]); + if (!chained) + { + break; + } + } + } + else + { + slot = cache_first_free(); + } + return slot; +} + +/* The cache page for `lba`, opened if not cached */ +static struct cache_page *cache_open(uint32_t lba) +{ + uint8_t slot = cache_match(lba); + bool hit = slot < CACHE_PAGES; + struct cache_page *c; + uint8_t i, j; + + if (!hit) + { + slot = cache_first_free(); + if (slot == CACHE_PAGES) + { + slot = cache_evict(); + } + } + + c = &ftl.cache[slot]; + c->valid = true; + c->lba = lba - lba % sec_per_page(); + + if (!hit) + { + for (j = 0; j < CACHE_PAGES && ftl.order[j] != NO_SLOT && + !(c->lba < ftl.cache[ftl.order[j]].lba); j++) + { + } + if (j < CACHE_PAGES && ftl.order[j] != NO_SLOT) + { + for (i = CACHE_PAGES - 1; i >= j && i < CACHE_PAGES; i--) + { + ftl.order[i + 1] = ftl.order[i]; + } + } + ftl.order[j] = slot; + } + + for (i = 0; i < CACHE_PAGES; i++) + { + if (c->age < ftl.cache[i].age) + { + ftl.cache[i].age--; + } + } + c->age = CACHE_PAGES - 1; + return c; +} + +/* ---- reading ---- */ + +static int read_sectors(uint32_t lba, uint8_t *buf, uint32_t count) +{ + int ret = 0; + + while (count > 0) + { + uint32_t sec = remap_sector(lba, true); + uint32_t off = lba % sec_per_page(); + uint32_t len = sec_per_page() - off; + uint32_t page = lba - off; + uint8_t k; + + if (len > count) + { + len = count; + } + if (flash_read(sec, buf, NULL, len)) + { + ret = 1; + } + + /* the cache has the newest copy of what it holds */ + for (k = 0; k < CACHE_PAGES && ftl.order[k] != NO_SLOT; k++) + { + const struct cache_page *c = &ftl.cache[ftl.order[k]]; + + if (c->lba == page) + { + uint32_t i; + + for (i = 0; i < len && c->mask != 0; i++) + { + if (c->mask & ((uint32_t)1 << (off + i))) + { + memcpy(buf + i * FLASH_SECTOR_SIZE, + cache_data(c) + (off + i) * FLASH_SECTOR_SIZE, + FLASH_SECTOR_SIZE); + } + } + break; + } + } + + lba += len; + buf += len * FLASH_SECTOR_SIZE; + count -= len; + } + return ret; +} + +/* ---- writing ---- */ + +/* Put `n` sectors from `buf` into cache page `c`, from sector `off` */ +static void cache_put(struct cache_page *c, uint32_t off, const uint8_t *buf, + uint32_t n) +{ + memcpy(cache_data(c) + off * FLASH_SECTOR_SIZE, buf, n * FLASH_SECTOR_SIZE); + c->mask |= page_mask(n) << off; +} + +static void write_sectors(uint32_t lba, const uint8_t *buf, uint32_t count) +{ + uint32_t spp = sec_per_page(); + uint32_t off = lba % spp; + uint32_t left = count; + bool done = false; + uint8_t k; + + if (off != 0) + { + uint32_t n = spp - off; + struct cache_page *c; + + if (n > count) + { + n = count; + } + c = cache_open(lba); + cache_put(c, off, buf, n); + buf += n * FLASH_SECTOR_SIZE; + lba += n; + left -= n; + if (count <= CACHE_FIRST_MAX) + { + cache_write(c); + } + else + { + cache_flush(c); + } + } + + while (!done) + { + if (left < spp) + { + if (left > 0) + { + struct cache_page *c = cache_open(lba); + + cache_put(c, 0, buf, left); + cache_write(c); + } + done = true; + } + else + { + /* odd but original: on the length of the whole request */ + if (count <= CACHE_WRITE_MAX && !exch_at(lba)) + { + struct cache_page *c = cache_open(lba); + + cache_put(c, 0, buf, spp); + cache_write(c); + } + else + { + /* the page before this one goes first, so a sequential + * write continues the exchange block; odd but original: + * the first in the order is never looked at */ + for (k = CACHE_PAGES - 1; k > 0; k--) + { + uint8_t i = ftl.order[k]; + + if (i != NO_SLOT && ftl.cache[i].lba + spp == lba) + { + cache_flush(&ftl.cache[i]); + break; + } + } + k = cache_match(lba); + if (k < CACHE_PAGES) + { + cache_invalidate(&ftl.cache[k]); + } + write_page(lba, buf); + } + buf += spp * FLASH_SECTOR_SIZE; + lba += spp; + left -= spp; + } + } +} + +/* ---- mount ---- */ + +/* Read the bad-block table: the newest page of the two mirror blocks among + * the first BBT_SCAN_BLOCKS of the FTL's area. Mirrors left at different + * pages are rewritten from the newer, as the original does on a writable + * mount. */ +static enum ftl_b_error read_bbt(void) +{ + enum ftl_b_error err = FTL_B_OK; + uint32_t addr[2] = { UINT32_MAX, UINT32_MAX }; + uint8_t meta[PAGE_META_BYTES]; + uint32_t spb = sec_per_block(); + uint32_t pick, off0, off1; + int found = -1; + uint16_t blk, i; + + for (blk = ftl.config.first_block; + blk < ftl.config.first_block + BBT_SCAN_BLOCKS + && blk < ftl.total_blocks && found < 1; + blk++) + { + uint32_t off; + + for (off = 0; off < spb; off += sec_per_page()) + { + if (flash_read(block_sector(blk) + off, NULL, meta, 1) == 0) + { + if (field(meta, F_TAG) != TAG_TABLE) + { + break; + } + if (off == 0) + { + found++; + } + if (found >= 0) + { + addr[found] = block_sector(blk) + off; + } + } + } + } + + if (found < 0) + { + err = FTL_B_ERR_NO_TABLE; + } + else + { + off0 = addr[0] % spb; + off1 = addr[1] % spb; + pick = addr[0]; + if (off0 != off1 && addr[1] != UINT32_MAX + && (addr[0] == UINT32_MAX || off1 >= off0)) + { + pick = addr[1]; + } + + flash_read(pick, page_buf, meta, sec_per_page()); + + if (off0 != off1) + { + uint16_t a = field(meta, F_MIRROR_A), b = field(meta, F_MIRROR_B); + + if (sector_block(pick) == a) + { + addr[0] = block_sector(b); + addr[1] = block_sector(a); + } + else + { + addr[1] = block_sector(b); + addr[0] = block_sector(a); + } + + if (a < ftl.config.first_block || b < ftl.config.first_block || + a >= ftl.total_blocks || b >= ftl.total_blocks) + { + err = FTL_B_ERR_TABLE; + } + else if (ftl.writable) + { + flash_erase(addr[0]); + flash_program(addr[0], page_buf, meta, sec_per_page()); + flash_erase(addr[1]); + flash_program(addr[1], page_buf, meta, sec_per_page()); + } + } + } + + if (err == FTL_B_OK) + { + const uint8_t *tbl = page_buf; + + ftl.format_version = field(meta, F_VER); + ftl.sys_block[SYS_POWER] = field(meta, F_POWER_BLOCK); + ftl.sys_block[SYS_LAST] = field(meta, F_LAST_SYS_BLOCK); + ftl.logical_blocks = field(meta, F_LOGICAL); + + for (i = 0; + i < BBT_MAX && (tbl[2 * i] | (tbl[2 * i + 1] << 8)) != 0xffff; + i++) + { + ftl.bad[i] = (uint16_t)(tbl[2 * i] | (tbl[2 * i + 1] << 8)); + } + ftl.bad_count = i; + + ftl.sys_block[SYS_MIRROR_A] = + sector_block(addr[1] < addr[0] ? addr[1] : addr[0]); + ftl.sys_block[SYS_MIRROR_B] = + sector_block(addr[1] < addr[0] ? addr[0] : addr[1]); + ftl.bbt_page = (uint16_t)(addr[0] % spb); + + if ((ftl.format_version >> 8) != FORMAT_VERSION_MAJOR) + { + err = FTL_B_ERR_VERSION; + } + else if (ftl.logical_blocks > MAX_LOGICAL_BLOCKS || + ftl.sys_block[SYS_LAST] >= ftl.total_blocks) + { + err = FTL_B_ERR_TOO_BIG; + } + } + return err; +} + +/* Finish a refresh of a system block that power loss interrupted: its + * record says the block's content waits in a temporary block. Unlike the + * original, which also replays its "done" record - a copy of block 0 onto + * itself - only an unfinished copy is redone. */ +static void power_loss_recover(void) +{ + uint32_t base = block_sector(ftl.sys_block[SYS_POWER]); + uint8_t meta[FLASH_META_SIZE]; + uint32_t mode = 0, src = 0, dst = 0; + bool any = false; + uint32_t off; + + for (off = 0; off < sec_per_block(); off += sec_per_page()) + { + if (flash_read(base + off, page_buf, meta, 1) == 0) + { + if (field(meta, 0) != TAG_POWER) + { + break; + } + mode = page_buf[0] | page_buf[1] << 8 | page_buf[2] << 16 + | (uint32_t)page_buf[3] << 24; + src = page_buf[4] | page_buf[5] << 8 | page_buf[6] << 16 + | (uint32_t)page_buf[7] << 24; + dst = page_buf[8] | page_buf[9] << 8 | page_buf[10] << 16 + | (uint32_t)page_buf[11] << 24; + any = true; + } + } + + if (any && ftl.writable) + { + if (mode == PWR_MODE_COPY && src < ftl.geo->total_sectors + && dst < ftl.geo->total_sectors) + { + flash_erase(dst); + copy_sectors(src, dst, sec_per_block()); + } + flash_erase(base); + } +} + +/* The mount's working set lives in the cache buffers, free until the + * journal is replayed into them */ +#define SCAN_USED ((uint32_t *)cache_buf[0]) +#define SCAN_PAIRS ((uint16_t *)cache_buf[1]) + +static void mark_used(uint16_t blk, bool used) +{ + if (blk < ftl.total_blocks) + { + if (used) + { + SCAN_USED[blk / 32] |= (uint32_t)1 << (blk % 32); + } + else + { + SCAN_USED[blk / 32] &= ~((uint32_t)1 << (blk % 32)); + } + } +} + +static bool is_used(uint16_t blk) +{ + return (SCAN_USED[blk / 32] >> (blk % 32)) & 1; +} + +/* Remember the (source, destination) of the logical block now stored in + * `blk`, replacing the pair of the block it displaced, `old` */ +static void add_pair(uint16_t *npairs, uint16_t old, uint16_t src, uint16_t dst) +{ + uint16_t k; + + for (k = 0; k < RCV_PAIRS && SCAN_PAIRS[2 * k + 1] != old; k++) + { + } + if (k == RCV_PAIRS && *npairs < RCV_PAIRS) + { + k = (*npairs)++; + } + if (k < RCV_PAIRS) + { + SCAN_PAIRS[2 * k] = src; + SCAN_PAIRS[2 * k + 1] = dst; + } +} + +/* A data block claiming logical block `lb`: the newer of it and the block + * claiming it already wins */ +static void scan_data_block(uint16_t blk, const uint8_t *meta, uint16_t *npairs) +{ + uint16_t lb = field(meta, F_LBLOCK); + uint16_t old = ftl.remap[lb]; + + if (old == 0) + { + mark_used(blk, true); + ftl.remap[lb] = blk; + } + else + { + uint8_t old_meta[DATA_HEADER_FIELDS * FLASH_META_SIZE]; + uint16_t src, dst; + + flash_read(block_sector(old), NULL, old_meta, DATA_HEADER_FIELDS); + + if (ver_newer(field(meta, F_VER), field(old_meta, F_VER))) + { + mark_used(old, false); + mark_used(blk, true); + ftl.remap[lb] = blk; + src = field(meta, F_SRC); + dst = field(meta, F_SELF); + } + else + { + src = field(old_meta, F_SRC); + dst = field(old_meta, F_SELF); + } + add_pair(npairs, old, src, dst); + } +} + +/* A journal block: keep the JOURNAL_BLOCKS newest, oldest first, in + * journal_blk[] with their versions in vers[]. vers[0] == 0 is taken for a + * free first entry. */ +static void scan_journal_block(uint16_t blk, uint16_t ver, uint16_t *vers, + uint8_t *count) +{ + int i = JOURNAL_BLOCKS - 1; + int k; + + while (i > JOURNAL_BLOCKS - 1 - *count && !ver_newer(ver, vers[i])) + { + i--; + } + + if (i > JOURNAL_BLOCKS - 1 - *count) + { + if (vers[0] == 0 && *count != JOURNAL_BLOCKS) + { + (*count)++; + } + else + { + mark_used(ftl.journal_blk[0], false); + queue_in(ftl.journal_blk[0]); + } + for (k = 0; k < i; k++) + { + vers[k] = vers[k + 1]; + ftl.journal_blk[k] = ftl.journal_blk[k + 1]; + } + vers[i] = ver; + ftl.journal_blk[i] = blk; + mark_used(blk, true); + } + else if (i < 0) + { + queue_in(blk); + } + else + { + vers[i] = ver; + ftl.journal_blk[i] = blk; + mark_used(blk, true); + (*count)++; + } +} + +/* An exchange the mount found: `dst` was being written with the content of + * `src`. If it is not full, reopen it - or, if one of its last pages cannot + * be read, finish it into a new block now, taking unreadable pages from the + * source. When it is full, the exchange finished and its source is free. */ +static void recover_exchange(uint16_t src_blk, uint16_t dst_blk, uint8_t *nexch) +{ + uint32_t spp = sec_per_page(); + uint32_t dst = block_sector(dst_blk), src = block_sector(src_blk); + int last = sec_per_block() / spp - 1; + int lo = 0, hi = last; + uint8_t meta[2 * FLASH_META_SIZE]; + uint16_t ver = 0; + uint32_t written, off; + uint16_t lb; + + flash_read(dst + spp * last, NULL, meta, 1); + if (field(meta, F_TAG) == FIELD_ERASED) + { + while (lo <= hi) + { + int mid = (lo + hi) / 2; + + flash_read(dst + spp * mid, NULL, meta, 2); + if (field(meta, F_TAG) == FIELD_ERASED) + { + hi = mid - 1; + } + else + { + ver = field(meta, F_VER); + lo = mid + 1; + } + } + } + written = spp * (hi + 1); + + if (written < sec_per_block()) + { + off = written > RECHECK_PAGES * spp ? written - RECHECK_PAGES * spp : 0; + while (off < written && flash_read(dst + off, NULL, NULL, spp) == 0) + { + off += spp; + } + + if (off < written && ftl.writable) + { + uint16_t nb = queue_out(); + uint32_t to; + + if (nb == 0) + { + nb = ftl.sys_block[SYS_POWER]; + } + to = block_sector(nb); + remap_set(remap_owner(dst), nb); + flash_erase(to); + copy_sectors(dst, to, off); + for (; off < written; off += spp) + { + if (flash_read(dst + off, NULL, NULL, spp) == 0) + { + copy_sectors(dst + off, to + off, spp); + } + else + { + copy_sectors(src + off, to + off, spp); + } + } + copy_sectors(src + written, to + written, + sec_per_block() - written); + queue_in(dst_blk); + queue_in(src_blk); + + /* odd but original: the block stays mapped to the power-loss + * block, which is then erased */ + if (nb == ftl.sys_block[SYS_POWER]) + { + copy_sectors(to, block_sector(queue_out()), sec_per_block()); + flash_erase(to); + } + } + else if (*nexch < ftl.config.exch_blocks) + { + struct exch_block *e = &ftl.exch[*nexch]; + + e->src = src; + e->start = dst + written; + e->end = dst + sec_per_block(); + e->ver = ver; + e->age = *nexch; + e->open = true; + (*nexch)++; + } + } + else if (src_blk < ftl.total_blocks) + { + /* finished: its source is free unless something maps it. The + * original would queue a source past the chip too. */ + for (lb = 0; lb < ftl.logical_blocks && ftl.remap[lb] != src_blk; lb++) + { + } + if (lb == ftl.logical_blocks) + { + queue_in(src_blk); + } + } +} + +/* Replay the journal into the cache: newest block first, newest page + * first, each page once, until the cache is full. A page with an empty + * mask cancels the older entries of its page. */ +static void replay_journal(void) +{ + uint32_t *seen = (uint32_t *)page_buf; + uint8_t meta[4 * FLASH_META_SIZE]; + uint8_t restored = 0; + int i; + + ftl.journal_page = NO_JOURNAL; + ftl.journal_slot = -1; + memset(page_buf, 0xff, sizeof(page_buf)); + + for (i = JOURNAL_BLOCKS - 1; i >= 0; i--) + { + int32_t off; + + if (ftl.journal_blk[i] == 0) + { + continue; + } + if (ftl.journal_slot < 0) + { + ftl.journal_slot = (int8_t)i; + } + + for (off = sec_per_block() - sec_per_page(); + off >= 0 && restored < CACHE_PAGES; + off -= sec_per_page()) + { + uint32_t sec = block_sector(ftl.journal_blk[i]) + off; + uint32_t lba, mask; + uint16_t k; + + if (flash_read(sec + F_LBA_LO, NULL, meta, 4) != 0) + { + continue; + } + /* byte 2 of the LBA word is never 0xff in a written page */ + if (meta[2] == 0xff) + { + ftl.journal_page = (uint32_t)off; + continue; + } + + lba = field(meta, 0) | (uint32_t)field(meta, 1) << 16; + mask = field(meta, 2) | (uint32_t)field(meta, 3) << 16; + + for (k = 0; + k < SEEN_MAX && seen[k] != 0xffffffff && seen[k] != lba; + k++) + { + } + if (k < SEEN_MAX && seen[k] == 0xffffffff) + { + if (mask != 0) + { + struct cache_page *c = cache_open(lba); + + restored++; + c->mask = mask; + flash_read(sec, cache_data(c), NULL, sec_per_page()); + } + seen[k] = lba; + } + } + } +} + +static enum ftl_b_error mount_scan(void) +{ + enum ftl_b_error err = FTL_B_OK; + uint16_t vers[JOURNAL_BLOCKS]; + uint8_t meta[DATA_HEADER_FIELDS * FLASH_META_SIZE]; + uint16_t npairs = 0; + uint8_t njournal = 0, nexch = 0; + uint16_t blk, k; + uint8_t i; + + ftl.total_blocks = (uint16_t)(ftl.geo->total_sectors / sec_per_block()); + ftl.free.max = ftl.total_blocks >> 5; + if (ftl.free.max < FREE_QUEUE_MIN) + { + ftl.free.max = FREE_QUEUE_MIN; + } + if (ftl.free.max > FREE_QUEUE_MAX) + { + ftl.free.max = FREE_QUEUE_MAX; + } + ftl.free.front = ftl.free.rear = ftl.free.count = 0; + + for (i = 0; i < MAX_EXCH_BLOCKS; i++) + { + ftl.exch[i].open = false; + ftl.exch[i].age = i; + } + for (i = 0; i < CACHE_PAGES; i++) + { + ftl.cache[i].valid = false; + ftl.cache[i].age = i; + ftl.cache[i].mask = 0; + ftl.cache[i].lba = 0; + ftl.order[i] = NO_SLOT; + } + ftl.order[CACHE_PAGES] = NO_SLOT; + + err = read_bbt(); + + if (err == FTL_B_OK) + { + memset(SCAN_USED, 0, (ftl.total_blocks + 31) / 32 * 4); + memset(SCAN_PAIRS, 0, RCV_PAIRS * 2 * sizeof(uint16_t)); + memset(ftl.remap, 0, sizeof(ftl.remap)); + memset(ftl.journal_blk, 0, sizeof(ftl.journal_blk)); + memset(vers, 0, sizeof(vers)); + + power_loss_recover(); + + for (blk = ftl.sys_block[SYS_LAST] + 1; blk < ftl.total_blocks; blk++) + { + uint16_t tag; + + if (block_is_bad(blk)) + { + mark_used(blk, true); + continue; + } + if (flash_read(block_sector(blk), NULL, meta, + DATA_HEADER_FIELDS) != 0) + { + continue; + } + + tag = field(meta, F_TAG); + if ((tag & TAG_KIND) == TAG_CACHE) + { + scan_journal_block(blk, field(meta, F_VER), vers, &njournal); + } + else if (tag == TAG_PAGE_REMAP) + { + queue_in(blk); + } + else if ((tag & TAG_KIND) == TAG_DATA + && field(meta, F_LBLOCK) < ftl.logical_blocks) + { + scan_data_block(blk, meta, &npairs); + } + } + + ftl.journal_ver = vers[JOURNAL_BLOCKS - 1]; + if (ftl.journal_blk[0] == 0) + { + /* close the gaps: oldest first from entry 0 */ + for (i = 0; i < JOURNAL_BLOCKS && ftl.journal_blk[i] == 0; i++) + { + } + for (k = 0; i < JOURNAL_BLOCKS; k++, i++) + { + ftl.journal_blk[k] = ftl.journal_blk[i]; + ftl.journal_blk[i] = 0; + } + } + + /* both blocks of an exchange stay out of the free queue */ + for (k = 0; k < npairs; k++) + { + mark_used(SCAN_PAIRS[2 * k], true); + mark_used(SCAN_PAIRS[2 * k + 1], true); + } + for (blk = ftl.sys_block[SYS_LAST] + 1; blk < ftl.total_blocks; blk++) + { + if (!is_used(blk)) + { + queue_in(blk); + } + } + + for (k = 0; k < RCV_PAIRS; k++) + { + uint16_t src = SCAN_PAIRS[2 * k], dst = SCAN_PAIRS[2 * k + 1]; + + /* the original reads past the chip for a destination that names + * no block; such a pair is skipped. A source past the chip - + * 0xffff in the blocks of a firmware image - only matters if the + * destination is not full. */ + if (dst != 0 && dst < ftl.total_blocks) + { + recover_exchange(src, dst, &nexch); + } + } + + replay_journal(); + } + return err; +} + +/* ---- interface ---- */ + +enum ftl_b_error ftl_b_mount(const struct ftl_b_config *config) +{ + enum ftl_b_error err = FTL_B_OK; + + ftl.mounted = false; + ftl.config = *config; + ftl.geo = flash_get_geometry(); + ftl.writable = !config->read_only; + + if (ftl.geo->sec_per_page < MIN_SEC_PER_PAGE || + ftl.geo->sec_per_page > FLASH_MAX_SEC_PER_PAGE || + ftl.geo->sec_per_block == 0 || + ftl.geo->total_sectors / ftl.geo->sec_per_block > MAX_BLOCKS) + { + err = FTL_B_ERR_GEOMETRY; + } + else if (config->exch_blocks == 0 + || config->exch_blocks > MAX_EXCH_BLOCKS + || config->first_block == 0) + { + err = FTL_B_ERR_CONFIG; + } + else + { + flash_set_meta_passthrough(true); + flash_set_boot_area(block_sector(config->first_block)); + flash_set_writable(ftl.writable); + + err = mount_scan(); + ftl.mounted = err == FTL_B_OK; + } + + if (!ftl.mounted) + { + ftl.writable = false; + flash_set_writable(false); + } + ftl.error = err; + return err; +} + +void ftl_b_get_status(struct ftl_b_status *status) +{ + uint8_t i; + + memset(status, 0, sizeof(*status)); + status->error = ftl.error; + status->writable = ftl.writable; + status->format_version = ftl.format_version; + status->logical_blocks = ftl.logical_blocks; + status->bad_blocks = ftl.bad_count; + status->free_blocks = ftl.free.count; + for (i = 0; ftl.mounted && i < ftl.config.exch_blocks; i++) + { + status->open_exch += ftl.exch[i].open; + } + for (i = 0; ftl.mounted && i < CACHE_PAGES; i++) + { + status->cached_pages += ftl.cache[i].valid; + } +} + +uint32_t ftl_b_capacity(void) +{ + uint32_t sectors = 0; + + if (ftl.mounted) + { + sectors = (uint32_t)ftl.logical_blocks * sec_per_block(); + } + return sectors; +} + +static bool range_valid(uint32_t sector, uint32_t count) +{ + uint32_t cap = ftl_b_capacity(); + + return sector < cap && count <= cap - sector; +} + +int ftl_b_read(uint32_t sector, void *buf, uint32_t count) +{ + int ret = 1; + + if (range_valid(sector, count)) + { + ret = read_sectors(sector, buf, count); + } + return ret; +} + +int ftl_b_write(uint32_t sector, const void *buf, uint32_t count) +{ + int ret = 1; + + if (ftl.writable && range_valid(sector, count)) + { + write_sectors(sector, buf, count); + ret = 0; + } + return ret; +} + +void ftl_b_sync(void) +{ +} diff --git a/firmware/target/arm/rk27xx/ftl-scheme-b.h b/firmware/target/arm/rk27xx/ftl-scheme-b.h new file mode 100644 index 0000000000..0d2a059ecb --- /dev/null +++ b/firmware/target/arm/rk27xx/ftl-scheme-b.h @@ -0,0 +1,87 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* The "Scheme B" flash translation layer of rk27xx devices - the + * self-describing on-flash format the original firmware of the HiFiMAN + * HM-601 and similar players uses, read and written compatibly so that the + * original firmware keeps working on the same media. A reimplementation + * from reverse engineering; see ftl-scheme-b.c for how it works. */ + +#ifndef __FTL_SCHEME_B_H__ +#define __FTL_SCHEME_B_H__ + +#include +#include + +struct ftl_b_config +{ + /* The first block the FTL owns; the boot area is everything before it. + * Not recorded in the FTL's own structures. */ + uint16_t first_block; + /* Exchange blocks open at once. The original firmware's own count must + * not be exceeded: its mount recovers no more than that many, and loses + * the data of the rest. 8 on the HM-601. */ + uint8_t exch_blocks; + /* Never write, not even the repairs a mount normally makes. */ + bool read_only; +}; + +/* Why a mount failed */ +enum ftl_b_error +{ + FTL_B_OK = 0, + FTL_B_ERR_GEOMETRY, /* the chip is not one this FTL can map */ + FTL_B_ERR_CONFIG, /* an unusable configuration */ + FTL_B_ERR_NO_TABLE, /* no bad-block table: not formatted */ + FTL_B_ERR_TABLE, /* the bad-block table is damaged */ + FTL_B_ERR_VERSION, /* a format generation not supported */ + FTL_B_ERR_TOO_BIG, /* more logical blocks than fit in RAM */ +}; + +struct ftl_b_status +{ + enum ftl_b_error error; + bool writable; + uint16_t format_version; /* as recorded in the bad-block table */ + uint16_t logical_blocks; + uint16_t bad_blocks; + uint16_t free_blocks; /* erased or erasable, queued for use */ + uint8_t open_exch; /* exchange blocks in progress */ + uint8_t cached_pages; /* pages held in the write cache */ +}; + +/* Mount the media. Returns FTL_B_OK or the reason it failed. */ +enum ftl_b_error ftl_b_mount(const struct ftl_b_config *config); +void ftl_b_get_status(struct ftl_b_status *status); + +/* Sectors in the logical space; 0 when not mounted */ +uint32_t ftl_b_capacity(void); + +/* Return 0, or non-zero if the request was refused or a sector could not be + * read reliably. Sectors are 512 bytes. */ +int ftl_b_read(uint32_t sector, void *buf, uint32_t count); +int ftl_b_write(uint32_t sector, const void *buf, uint32_t count); + +/* Every write is on the flash when ftl_b_write() returns - the write cache + * is journalled as it changes - so there is nothing to write out. Kept for + * the storage layer's interface. */ +void ftl_b_sync(void); + +#endif /* __FTL_SCHEME_B_H__ */ From e83b7d6dc4eace8eeb667bcdcd734d8f7dabbc05 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 09:28:08 +0200 Subject: [PATCH 297/408] rk27xx: add a finder for the NAND's FTL scheme Many rk27xx targets have NAND whose format nobody has examined. The finder reads ID block 1 and the first page of the first 512 blocks and says which FTL formatted them: Scheme A by its remap-log blocks, Scheme B by its bad-block table and data headers, another Scheme B generation by other 0xFxxx tags. The later ID block layout ('RK27' at 0x0a) records the FTL area's BCH strength at 0x1ed - 8 on the HM-601, 14 on the Archos Vision 28 - and the scan reads in that mode. It is read-only, shown in the debug menu as "View FTL scheme", and built for targets whose NAND is not storage - none yet. Run on dumps of an HM-601 it reports Scheme B, a Samsung YP-CP3 Scheme A, and an Archos Vision 28 the other Scheme B generation. Co-Authored-By: Claude Opus 5.5 Change-Id: I0528f9d2a6996089b6488a77b43942f6fd1f2c16 --- apps/debug_menu.c | 56 +++++ firmware/SOURCES | 5 + firmware/target/arm/rk27xx/ftl-probe-rk27xx.c | 219 ++++++++++++++++++ firmware/target/arm/rk27xx/ftl-probe-rk27xx.h | 83 +++++++ 4 files changed, 363 insertions(+) create mode 100644 firmware/target/arm/rk27xx/ftl-probe-rk27xx.c create mode 100644 firmware/target/arm/rk27xx/ftl-probe-rk27xx.h diff --git a/apps/debug_menu.c b/apps/debug_menu.c index 95e744f160..db8203643f 100644 --- a/apps/debug_menu.c +++ b/apps/debug_menu.c @@ -2857,6 +2857,58 @@ static bool view_ram_info(void) } #endif +#if (CONFIG_PLATFORM & PLATFORM_NATIVE) && (CONFIG_NAND == NAND_RK27XX) \ + && !(CONFIG_STORAGE & STORAGE_NAND) +#include "ftl-probe-rk27xx.h" + +/* The target does not know its NAND's FTL scheme yet: show what the + * finder sees, for the user to report */ +static bool dbg_ftl_scheme(void) +{ + struct ftl_probe p; + struct simplelist_info info; + + ftl_probe(&p); + simplelist_info_init(&info, "FTL scheme", 0, NULL); + simplelist_reset_lines(); + simplelist_addline("Scheme: %s", ftl_probe_scheme_name(p.scheme)); + if (p.flash_error) + { + simplelist_addline("No usable NAND (%d)", p.flash_error); + } + else + { + simplelist_addline("Chip: %lu blocks", (unsigned long)p.blocks); + simplelist_addline("%u planes, %u/%u sec", p.planes, p.sec_per_page, + p.sec_per_block); + if (p.idb_boot_blocks) + { + simplelist_addline("IDB: %u blk %u+%u MB", p.idb_boot_blocks, + p.idb_sys_mb, p.idb_data_mb); + if (p.idb_rk27) + simplelist_addline("IDB: RK27, ECC t=%u", p.idb_ecc_t); + else + simplelist_addline("IDB: early layout"); + } + else + simplelist_addline("IDB: none"); + simplelist_addline("Scanned %u blocks, t=%u", p.scanned, p.scan_ecc_t); + simplelist_addline("Erased %u, bad ECC %u", p.erased, p.unreadable); + simplelist_addline("A: %u logs, %u used", p.a_logs, p.a_blocks); + simplelist_addline("B: %u tables at %u", p.b_tables, + p.b_tables ? p.first_b_table : 0); + simplelist_addline("B: %u cache, %u data", p.b_cache, p.b_data); + if (p.b_other) + simplelist_addline("B: %u other, %04x", p.b_other, p.first_b_other); + if (p.other) + simplelist_addline("Other: %u, %u %02x%02x%02x", p.other, + p.first_other, p.first_other_meta[0], + p.first_other_meta[1], p.first_other_meta[2]); + } + return simplelist_show_list(&info); +} +#endif + /****** The menu *********/ static const struct { unsigned char *desc; /* string or ID */ @@ -2909,6 +2961,10 @@ static const struct { #if ((CONFIG_PLATFORM & PLATFORM_NATIVE) || defined(SONY_NWZ_LINUX) || defined(HIBY_LINUX) || defined(FIIO_M3K_LINUX)) && !defined(SIMULATOR) { "View HW info", dbg_hw_info }, #endif +#if (CONFIG_PLATFORM & PLATFORM_NATIVE) && (CONFIG_NAND == NAND_RK27XX) \ + && !(CONFIG_STORAGE & STORAGE_NAND) + { "View FTL scheme", dbg_ftl_scheme }, +#endif #if (CONFIG_PLATFORM & PLATFORM_NATIVE) { "View partitions", dbg_partitions }, #endif diff --git a/firmware/SOURCES b/firmware/SOURCES index 0bfa9f45cf..46966bb507 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1714,6 +1714,11 @@ target/arm/rk27xx/ftl-rk27xx.c target/arm/rk27xx/ftl-scheme-a.c target/arm/rk27xx/flash-rk27xx.c target/arm/rk27xx/nand-rk27xx.c +#if (CONFIG_NAND == NAND_RK27XX) && !(CONFIG_STORAGE & STORAGE_NAND) \ + && !defined(BOOTLOADER) +/* no FTL scheme configured: the finder (debug menu) */ +target/arm/rk27xx/ftl-probe-rk27xx.c +#endif target/arm/rk27xx/usb-rk27xx.c target/arm/rk27xx/lcdif-rk27xx.c target/arm/rk27xx/rkw-loader.c diff --git a/firmware/target/arm/rk27xx/ftl-probe-rk27xx.c b/firmware/target/arm/rk27xx/ftl-probe-rk27xx.c new file mode 100644 index 0000000000..3b04d08f65 --- /dev/null +++ b/firmware/target/arm/rk27xx/ftl-probe-rk27xx.c @@ -0,0 +1,219 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* The FTL scheme finder - see ftl-probe-rk27xx.h. + * + * The two formats tell themselves apart in the metadata of a block's first + * sector (flash-rk27xx.h): Scheme A marks its remap-log blocks with type + * 0x52 in byte 2 (ftl-scheme-a.c), Scheme B keeps a 0xfxxx tag in bytes 0-1 + * - 0xf000 for its bad-block table near the start of the chip, 0xf100 and + * 0xf200 for its cache and data blocks (ftl-scheme-b.c). Every zone of + * Scheme A has two log blocks, the first zone within its first 256 blocks; + * Scheme B's table lies within 50 blocks of the boot area. So the first + * SCAN_BLOCKS blocks decide. + * + * They are read in the BCH mode the FTL's area uses, which need not be the + * boot area's t=8: the Archos Vision writes its FTL area with t=14. ID + * block 1 of the later generation - 'RK27' at 0x0a - records it at 0x1ed + * (8 on the HM-601, 14 on the Archos); the earlier generation's ID block + * has no such field, and its devices are t=8 throughout. */ + +#include +#include + +#include "config.h" +#include "nand-target.h" +#include "flash-rk27xx.h" +#include "ftl-probe-rk27xx.h" + +#define SCAN_BLOCKS 512 + +/* ID blocks: every IDB_STRIDE-th raw sector of the boot area, metadata type + * IDB_TYPE; ID block 1 is the sector after (ftl-rk27xx.c) */ +#define IDB_STRIDE 512 +#define IDB_POSITIONS 50 +#define IDB_TYPE 0x69 +#define IDB1_MAGIC 0x0a /* 'RK27' in the later generation's ... */ +#define IDB1_ECC_T 0x1ed /* ... which records the BCH t here */ +#define DEFAULT_ECC_T 8 + +#define A_LOG_TYPE 0x52 +#define B_TAG_TABLE 0xf000 +#define B_TAG_CACHE 0xf100 +#define B_TAG_DATA 0xf200 + +static void probe_idb(struct ftl_probe *p) +{ + uint8_t data[FLASH_SECTOR_SIZE], meta[FLASH_META_SIZE]; + uint32_t pos; + bool found = false; + + for (pos = 0; pos < IDB_POSITIONS && !found; pos++) + { + uint32_t raw = pos * IDB_STRIDE; + + if (flash_read_raw(raw, data, meta) == 0 && meta[2] == IDB_TYPE && + flash_read_raw(raw + 1, data, meta) == 0) + { + p->idb_boot_blocks = (uint16_t)(data[0] | data[1] << 8); + p->idb_sys_mb = (uint16_t)(data[2] | data[3] << 8); + p->idb_data_mb = (uint16_t)(data[4] | data[5] << 8); + p->idb_rk27 = memcmp(data + IDB1_MAGIC, "RK27", 4) == 0; + if (p->idb_rk27) + { + p->idb_ecc_t = data[IDB1_ECC_T]; + } + found = true; + } + } +} + +static void probe_block(struct ftl_probe *p, uint32_t blk) +{ + uint8_t meta[FLASH_META_SIZE]; + uint16_t tag; + + if (flash_read(blk * p->sec_per_block, NULL, meta, 1) != 0) + { + p->unreadable++; + } + else + { + tag = (uint16_t)(meta[0] | meta[1] << 8); + + /* tag 0xffff: erased, or a block only its byte 2 marks - an ID + * block, type 0x69 */ + if (tag == 0xffff) + { + p->erased++; + } + else if (meta[0] == 0xff && meta[1] == 0x00 && meta[2] == A_LOG_TYPE) + { + p->a_logs++; + } + else if (meta[1] == 0x00) + { + /* Scheme A's flash layer zeroes byte 1 of every page */ + p->a_blocks++; + } + else if (tag == B_TAG_TABLE) + { + if (p->b_tables++ == 0) + { + p->first_b_table = (uint16_t)blk; + } + } + else if ((tag & 0xff00) == B_TAG_CACHE) + { + p->b_cache++; + } + else if ((tag & 0xff00) == B_TAG_DATA) + { + p->b_data++; + } + else if ((tag & 0xf000) == 0xf000) + { + if (p->b_other++ == 0) + { + p->first_b_other = tag; + } + } + else if (p->other++ == 0) + { + p->first_other = (uint16_t)blk; + memcpy(p->first_other_meta, meta, sizeof(p->first_other_meta)); + } + } +} + +void ftl_probe(struct ftl_probe *p) +{ + const struct flash_geometry *geo; + uint32_t blk; + + memset(p, 0, sizeof(*p)); + p->first_b_table = 0xffff; + + flash_init(); + p->flash_error = flash_layer_init(); + + if (p->flash_error == 0) + { + geo = flash_get_geometry(); + p->planes = geo->planes; + p->sec_per_page = geo->sec_per_page; + p->sec_per_block = geo->sec_per_block; + p->blocks = geo->total_blocks; + + probe_idb(p); + + /* a value the controller has no mode for is reported, and t=8 used */ + p->scan_ecc_t = DEFAULT_ECC_T; + if (p->idb_rk27 && flash_set_ecc(p->idb_ecc_t) == 0) + { + p->scan_ecc_t = p->idb_ecc_t; + } + + for (blk = 0; blk < SCAN_BLOCKS && blk < p->blocks; blk++) + { + probe_block(p, blk); + } + p->scanned = (uint16_t)blk; + flash_set_ecc(DEFAULT_ECC_T); + + if (p->b_tables > 0 && p->b_data > 0) + { + p->scheme = p->b_other > 0 ? FTL_PROBE_SCHEME_B_OTHER + : FTL_PROBE_SCHEME_B; + } + else if (p->a_logs >= 2 && p->b_tables == 0) + { + p->scheme = FTL_PROBE_SCHEME_A; + } + else if (p->unreadable > p->scanned / 2) + { + p->scheme = FTL_PROBE_UNREADABLE; + } + } +} + +const char *ftl_probe_scheme_name(enum ftl_probe_scheme scheme) +{ + const char *name = "unknown"; + + switch (scheme) + { + case FTL_PROBE_SCHEME_A: + name = "A"; + break; + case FTL_PROBE_SCHEME_B: + name = "B"; + break; + case FTL_PROBE_UNREADABLE: + name = "unknown, ECC fails"; + break; + case FTL_PROBE_SCHEME_B_OTHER: + name = "B, other generation"; + break; + default: + break; + } + return name; +} diff --git a/firmware/target/arm/rk27xx/ftl-probe-rk27xx.h b/firmware/target/arm/rk27xx/ftl-probe-rk27xx.h new file mode 100644 index 0000000000..cabd75a310 --- /dev/null +++ b/firmware/target/arm/rk27xx/ftl-probe-rk27xx.h @@ -0,0 +1,83 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* Which flash translation layer formatted an rk27xx target's NAND - for + * targets whose config does not define CONFIG_RK27XX_FTL yet. Read-only: + * it looks at the first page of the first blocks and nothing else. */ + +#ifndef __FTL_PROBE_RK27XX_H__ +#define __FTL_PROBE_RK27XX_H__ + +#include +#include + +enum ftl_probe_scheme +{ + FTL_PROBE_UNKNOWN = 0, + FTL_PROBE_SCHEME_A, /* remap-log blocks: ftl-scheme-a.c */ + FTL_PROBE_SCHEME_B, /* bad-block table and data headers: + * ftl-scheme-b.c */ + FTL_PROBE_SCHEME_B_OTHER, /* Scheme B tags of another generation */ + FTL_PROBE_UNREADABLE, /* most blocks fail ECC in the mode used */ +}; + +struct ftl_probe +{ + enum ftl_probe_scheme scheme; + int flash_error; /* flash_layer_init() result; 0 = chip found */ + + /* the chip */ + uint8_t planes; + uint8_t sec_per_page; /* both planes */ + uint16_t sec_per_block; + uint32_t blocks; + + /* ID block 1, 0 if none found */ + uint16_t idb_boot_blocks; /* raw blocks */ + uint16_t idb_sys_mb; + uint16_t idb_data_mb; + bool idb_rk27; /* the later layout, with 'RK27' at 0x0a ... */ + uint8_t idb_ecc_t; /* ... and the FTL area's BCH t at 0x1ed */ + + uint8_t scan_ecc_t; /* the BCH t the blocks were read with */ + + /* what page 0 of the blocks scanned holds */ + uint16_t scanned; + uint16_t unreadable; + uint16_t erased; + uint16_t a_logs; /* Scheme A remap-log blocks */ + uint16_t a_blocks; /* other blocks Scheme A has programmed */ + uint16_t b_tables; /* Scheme B: 0xf000 bad-block table */ + uint16_t b_cache; /* 0xf1xx */ + uint16_t b_data; /* 0xf2xx */ + uint16_t b_other; /* other 0xfxxx tags */ + uint16_t other; /* none of these */ + uint16_t first_b_table; /* block of the first 0xf000; 0xffff = none */ + uint16_t first_b_other; /* the first other 0xfxxx tag; 0 = none */ + uint16_t first_other; /* first block of none of these ... */ + uint8_t first_other_meta[3]; /* ... and its metadata */ +}; + +/* Run the probe; takes a fraction of a second */ +void ftl_probe(struct ftl_probe *p); + +const char *ftl_probe_scheme_name(enum ftl_probe_scheme scheme); + +#endif /* __FTL_PROBE_RK27XX_H__ */ From c453f4fa8fa01451a472e7005c57ed241b439e45 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 09:28:09 +0200 Subject: [PATCH 298/408] rk27xx: name the NAND's FTL scheme in each target's config CONFIG_RK27XX_FTL selects the FTL a target's NAND uses, RK27XX_FTL_SCHEME_A or RK27XX_FTL_SCHEME_B. rk27generic and the YP-CP3 are Scheme A, the HM-60x Scheme B. ftl-rk27xx.c mounts the one named; for Scheme B it maps the drives onto the volumes ID block 1 records: the system disk from LBA 0, the user volume after the system data area. The other rk27xx targets with NAND - HM-801, MA8, MA8C, MA9, MA9C and iHiFi 760, 770, 770C, 800, 960 - have no confirmed scheme. They drop the NAND from storage and build only the FTL scheme finder, so users can report what their device holds and the scheme can then be set. Only Scheme A flushes at shutdown: Scheme B holds nothing in RAM. Co-Authored-By: Claude Opus 5.5 Change-Id: I484217b82c6de7316b90b354c012bfbaa263b3dd --- firmware/SOURCES | 18 +- firmware/export/config.h | 13 +- firmware/export/config/hifietma8.h | 8 +- firmware/export/config/hifietma8c.h | 8 +- firmware/export/config/hifietma9.h | 8 +- firmware/export/config/hifietma9c.h | 8 +- firmware/export/config/hifimanhm60x.h | 1 + firmware/export/config/hifimanhm801.h | 8 +- firmware/export/config/ihifi760.h | 8 +- firmware/export/config/ihifi770.h | 8 +- firmware/export/config/ihifi770c.h | 8 +- firmware/export/config/ihifi800.h | 8 +- firmware/export/config/ihifi960.h | 8 +- firmware/export/config/rk27generic.h | 1 + firmware/export/config/samsungypcp3.h | 1 + firmware/target/arm/rk27xx/ftl-rk27xx.c | 242 +++++++++++++++++++----- 16 files changed, 278 insertions(+), 78 deletions(-) diff --git a/firmware/SOURCES b/firmware/SOURCES index 46966bb507..cd1c3f201a 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1710,15 +1710,23 @@ target/arm/rk27xx/system-rk27xx.c target/arm/rk27xx/backlight-rk27xx.c target/arm/rk27xx/adc-rk27xx.c target/arm/rk27xx/sd-rk27xx.c -target/arm/rk27xx/ftl-rk27xx.c -target/arm/rk27xx/ftl-scheme-a.c +#if CONFIG_NAND == NAND_RK27XX +#if (CONFIG_STORAGE & STORAGE_NAND) +target/arm/rk27xx/flash-rk27xx.c +target/arm/rk27xx/nand-rk27xx.c +target/arm/rk27xx/ftl-rk27xx.c +#if CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_A +target/arm/rk27xx/ftl-scheme-a.c +#elif CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_B +target/arm/rk27xx/ftl-scheme-b.c +#endif +#elif !defined(BOOTLOADER) +/* no FTL scheme configured: only the finder (debug menu) */ target/arm/rk27xx/flash-rk27xx.c target/arm/rk27xx/nand-rk27xx.c -#if (CONFIG_NAND == NAND_RK27XX) && !(CONFIG_STORAGE & STORAGE_NAND) \ - && !defined(BOOTLOADER) -/* no FTL scheme configured: the finder (debug menu) */ target/arm/rk27xx/ftl-probe-rk27xx.c #endif +#endif target/arm/rk27xx/usb-rk27xx.c target/arm/rk27xx/lcdif-rk27xx.c target/arm/rk27xx/rkw-loader.c diff --git a/firmware/export/config.h b/firmware/export/config.h index 508caf5e40..786ca1e3a8 100644 --- a/firmware/export/config.h +++ b/firmware/export/config.h @@ -325,6 +325,11 @@ Lyre prototype 1 */ #define NAND_RK27XX 5 #define NAND_IMX233 6 +/* CONFIG_RK27XX_FTL: the on-flash format of an rk27xx target's NAND. A + * target that does not define it builds only the FTL scheme finder. */ +#define RK27XX_FTL_SCHEME_A 1 /* ftl-scheme-a.c: zone tables, remap logs */ +#define RK27XX_FTL_SCHEME_B 2 /* ftl-scheme-b.c: self-describing blocks */ + /* CONFIG_RTC */ #define RTC_HOSTED 1 /* Generic hosted */ #define RTC_PCF50605 2 /* iPod 3G, 4G & Mini */ @@ -893,10 +898,12 @@ Lyre prototype 1 */ /* Storage related config handling */ -/* The rk27xx NAND's flash translation layer holds part-written pages in RAM - * (ftl-scheme-a.c) until a later write completes them; storage_flush() - * commits them at shutdown, ROLO and wherever else it is called. */ +/* The rk27xx NAND's Scheme A flash translation layer holds part-written + * pages in RAM (ftl-scheme-a.c) until a later write completes them; + * storage_flush() commits them at shutdown, ROLO and wherever else it is + * called. */ #if (CONFIG_STORAGE & STORAGE_NAND) && (CONFIG_NAND == NAND_RK27XX) \ + && (CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_A) \ && !defined(HAVE_STORAGE_FLUSH) #define HAVE_STORAGE_FLUSH #endif diff --git a/firmware/export/config/hifietma8.h b/firmware/export/config/hifietma8.h index 80f6be3b9e..b40c22a4c0 100644 --- a/firmware/export/config/hifietma8.h +++ b/firmware/export/config/hifietma8.h @@ -45,7 +45,11 @@ /* define this if you have a flash memory storage */ #define HAVE_FLASH_STORAGE -#define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) +/* The NAND's FTL scheme is not known yet, so the NAND is not storage here: + * only the FTL scheme finder is built (Debug > View FTL scheme). Once a + * report confirms the scheme, define CONFIG_RK27XX_FTL and add STORAGE_NAND, + * as config/hifimanhm60x.h does. */ +#define CONFIG_STORAGE STORAGE_SD #define CONFIG_NAND NAND_RK27XX #define HAVE_SW_TONE_CONTROLS @@ -53,7 +57,7 @@ /* commented for now */ /* #define HAVE_HOTSWAP */ -#define NUM_DRIVES 2 +#define NUM_DRIVES 1 #define SECTOR_SIZE 512 /* for small(ish) SD cards */ diff --git a/firmware/export/config/hifietma8c.h b/firmware/export/config/hifietma8c.h index 8504fa1e55..1006d8721b 100644 --- a/firmware/export/config/hifietma8c.h +++ b/firmware/export/config/hifietma8c.h @@ -45,7 +45,11 @@ /* define this if you have a flash memory storage */ #define HAVE_FLASH_STORAGE -#define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) +/* The NAND's FTL scheme is not known yet, so the NAND is not storage here: + * only the FTL scheme finder is built (Debug > View FTL scheme). Once a + * report confirms the scheme, define CONFIG_RK27XX_FTL and add STORAGE_NAND, + * as config/hifimanhm60x.h does. */ +#define CONFIG_STORAGE STORAGE_SD #define CONFIG_NAND NAND_RK27XX #define HAVE_SW_TONE_CONTROLS @@ -53,7 +57,7 @@ /* commented for now */ /* #define HAVE_HOTSWAP */ -#define NUM_DRIVES 2 +#define NUM_DRIVES 1 #define SECTOR_SIZE 512 /* for small(ish) SD cards */ diff --git a/firmware/export/config/hifietma9.h b/firmware/export/config/hifietma9.h index 4c9eea2594..123a549f99 100644 --- a/firmware/export/config/hifietma9.h +++ b/firmware/export/config/hifietma9.h @@ -45,7 +45,11 @@ /* define this if you have a flash memory storage */ #define HAVE_FLASH_STORAGE -#define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) +/* The NAND's FTL scheme is not known yet, so the NAND is not storage here: + * only the FTL scheme finder is built (Debug > View FTL scheme). Once a + * report confirms the scheme, define CONFIG_RK27XX_FTL and add STORAGE_NAND, + * as config/hifimanhm60x.h does. */ +#define CONFIG_STORAGE STORAGE_SD #define CONFIG_NAND NAND_RK27XX #define HAVE_SW_TONE_CONTROLS @@ -53,7 +57,7 @@ /* commented for now */ /* #define HAVE_HOTSWAP */ -#define NUM_DRIVES 2 +#define NUM_DRIVES 1 #define SECTOR_SIZE 512 /* for small(ish) SD cards */ diff --git a/firmware/export/config/hifietma9c.h b/firmware/export/config/hifietma9c.h index bcc4505d54..b40d9bd4e9 100644 --- a/firmware/export/config/hifietma9c.h +++ b/firmware/export/config/hifietma9c.h @@ -45,7 +45,11 @@ /* define this if you have a flash memory storage */ #define HAVE_FLASH_STORAGE -#define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) +/* The NAND's FTL scheme is not known yet, so the NAND is not storage here: + * only the FTL scheme finder is built (Debug > View FTL scheme). Once a + * report confirms the scheme, define CONFIG_RK27XX_FTL and add STORAGE_NAND, + * as config/hifimanhm60x.h does. */ +#define CONFIG_STORAGE STORAGE_SD #define CONFIG_NAND NAND_RK27XX #define HAVE_SW_TONE_CONTROLS @@ -53,7 +57,7 @@ /* commented for now */ /* #define HAVE_HOTSWAP */ -#define NUM_DRIVES 2 +#define NUM_DRIVES 1 #define SECTOR_SIZE 512 /* for small(ish) SD cards */ diff --git a/firmware/export/config/hifimanhm60x.h b/firmware/export/config/hifimanhm60x.h index 51de435467..e26ff271fe 100644 --- a/firmware/export/config/hifimanhm60x.h +++ b/firmware/export/config/hifimanhm60x.h @@ -51,6 +51,7 @@ #define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) #define CONFIG_NAND NAND_RK27XX +#define CONFIG_RK27XX_FTL RK27XX_FTL_SCHEME_B #define HAVE_SW_TONE_CONTROLS #define HAVE_SW_VOLUME_CONTROL diff --git a/firmware/export/config/hifimanhm801.h b/firmware/export/config/hifimanhm801.h index d420a1f714..c604725de1 100644 --- a/firmware/export/config/hifimanhm801.h +++ b/firmware/export/config/hifimanhm801.h @@ -45,7 +45,11 @@ /* define this if you have a flash memory storage */ #define HAVE_FLASH_STORAGE -#define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) +/* The NAND's FTL scheme is not known yet, so the NAND is not storage here: + * only the FTL scheme finder is built (Debug > View FTL scheme). Once a + * report confirms the scheme, define CONFIG_RK27XX_FTL and add STORAGE_NAND, + * as config/hifimanhm60x.h does. */ +#define CONFIG_STORAGE STORAGE_SD #define CONFIG_NAND NAND_RK27XX #define HAVE_SW_TONE_CONTROLS @@ -55,7 +59,7 @@ /* commented for now */ /* #define HAVE_HOTSWAP */ -#define NUM_DRIVES 2 +#define NUM_DRIVES 1 #define SECTOR_SIZE 512 /* for small(ish) SD cards */ diff --git a/firmware/export/config/ihifi760.h b/firmware/export/config/ihifi760.h index b3057daaaf..e64b5403f8 100644 --- a/firmware/export/config/ihifi760.h +++ b/firmware/export/config/ihifi760.h @@ -48,8 +48,12 @@ /* define this if you have a flash memory storage */ #define HAVE_FLASH_STORAGE -#define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) -#define NUM_DRIVES 1 /* NAND doesn't work yet */ +/* The NAND's FTL scheme is not known yet, so the NAND is not storage here: + * only the FTL scheme finder is built (Debug > View FTL scheme). Once a + * report confirms the scheme, define CONFIG_RK27XX_FTL and add STORAGE_NAND, + * as config/hifimanhm60x.h does. */ +#define CONFIG_STORAGE STORAGE_SD +#define NUM_DRIVES 1 #define CONFIG_NAND NAND_RK27XX #define HAVE_SW_TONE_CONTROLS diff --git a/firmware/export/config/ihifi770.h b/firmware/export/config/ihifi770.h index d6ca563339..0c8ddae90d 100644 --- a/firmware/export/config/ihifi770.h +++ b/firmware/export/config/ihifi770.h @@ -49,8 +49,12 @@ /* define this if you have a flash memory storage */ #define HAVE_FLASH_STORAGE -#define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) -#define NUM_DRIVES 1 /* NAND doesn't work yet */ +/* The NAND's FTL scheme is not known yet, so the NAND is not storage here: + * only the FTL scheme finder is built (Debug > View FTL scheme). Once a + * report confirms the scheme, define CONFIG_RK27XX_FTL and add STORAGE_NAND, + * as config/hifimanhm60x.h does. */ +#define CONFIG_STORAGE STORAGE_SD +#define NUM_DRIVES 1 #define CONFIG_NAND NAND_RK27XX #define HAVE_SW_TONE_CONTROLS diff --git a/firmware/export/config/ihifi770c.h b/firmware/export/config/ihifi770c.h index c305a3a3d0..f55f120712 100644 --- a/firmware/export/config/ihifi770c.h +++ b/firmware/export/config/ihifi770c.h @@ -49,8 +49,12 @@ /* define this if you have a flash memory storage */ #define HAVE_FLASH_STORAGE -#define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) -#define NUM_DRIVES 1 /* NAND doesn't work yet */ +/* The NAND's FTL scheme is not known yet, so the NAND is not storage here: + * only the FTL scheme finder is built (Debug > View FTL scheme). Once a + * report confirms the scheme, define CONFIG_RK27XX_FTL and add STORAGE_NAND, + * as config/hifimanhm60x.h does. */ +#define CONFIG_STORAGE STORAGE_SD +#define NUM_DRIVES 1 #define CONFIG_NAND NAND_RK27XX #define HAVE_SW_TONE_CONTROLS diff --git a/firmware/export/config/ihifi800.h b/firmware/export/config/ihifi800.h index c6e8652111..d70c259c59 100644 --- a/firmware/export/config/ihifi800.h +++ b/firmware/export/config/ihifi800.h @@ -49,8 +49,12 @@ /* define this if you have a flash memory storage */ #define HAVE_FLASH_STORAGE -#define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) -#define NUM_DRIVES 1 /* NAND doesn't work yet */ +/* The NAND's FTL scheme is not known yet, so the NAND is not storage here: + * only the FTL scheme finder is built (Debug > View FTL scheme). Once a + * report confirms the scheme, define CONFIG_RK27XX_FTL and add STORAGE_NAND, + * as config/hifimanhm60x.h does. */ +#define CONFIG_STORAGE STORAGE_SD +#define NUM_DRIVES 1 #define CONFIG_NAND NAND_RK27XX #define HAVE_SW_TONE_CONTROLS diff --git a/firmware/export/config/ihifi960.h b/firmware/export/config/ihifi960.h index f1506f59df..a3af5d7354 100644 --- a/firmware/export/config/ihifi960.h +++ b/firmware/export/config/ihifi960.h @@ -48,8 +48,12 @@ /* define this if you have a flash memory storage */ #define HAVE_FLASH_STORAGE -#define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) -#define NUM_DRIVES 1 /* NAND doesn't work yet */ +/* The NAND's FTL scheme is not known yet, so the NAND is not storage here: + * only the FTL scheme finder is built (Debug > View FTL scheme). Once a + * report confirms the scheme, define CONFIG_RK27XX_FTL and add STORAGE_NAND, + * as config/hifimanhm60x.h does. */ +#define CONFIG_STORAGE STORAGE_SD +#define NUM_DRIVES 1 #define CONFIG_NAND NAND_RK27XX #define HAVE_SW_TONE_CONTROLS diff --git a/firmware/export/config/rk27generic.h b/firmware/export/config/rk27generic.h index 8c1eaa3975..a61f0400ac 100644 --- a/firmware/export/config/rk27generic.h +++ b/firmware/export/config/rk27generic.h @@ -58,6 +58,7 @@ #define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) #define CONFIG_NAND NAND_RK27XX +#define CONFIG_RK27XX_FTL RK27XX_FTL_SCHEME_A #define HAVE_SW_TONE_CONTROLS /* commented for now */ diff --git a/firmware/export/config/samsungypcp3.h b/firmware/export/config/samsungypcp3.h index 61b80f5477..658b327165 100644 --- a/firmware/export/config/samsungypcp3.h +++ b/firmware/export/config/samsungypcp3.h @@ -44,6 +44,7 @@ #define CONFIG_STORAGE (STORAGE_SD | STORAGE_NAND) #define CONFIG_NAND NAND_RK27XX +#define CONFIG_RK27XX_FTL RK27XX_FTL_SCHEME_A /* commented for now */ /* #define HAVE_HOTSWAP */ diff --git a/firmware/target/arm/rk27xx/ftl-rk27xx.c b/firmware/target/arm/rk27xx/ftl-rk27xx.c index e5e50a8cf6..1f5de9ca3f 100644 --- a/firmware/target/arm/rk27xx/ftl-rk27xx.c +++ b/firmware/target/arm/rk27xx/ftl-rk27xx.c @@ -19,7 +19,8 @@ * ****************************************************************************/ -/* Rockbox storage on the rk27xx Scheme A FTL (ftl-scheme-a.c). +/* Rockbox storage on the rk27xx NAND, through the FTL scheme the target's + * config names in CONFIG_RK27XX_FTL: ftl-scheme-a.c or ftl-scheme-b.c. * * Writing is opt-in: a build without FTL_ALLOW_WRITE mounts read-only and * never writes the flash, not even the repairs a mount can make. */ @@ -29,7 +30,14 @@ #include "ftl-target.h" #include "nand-target.h" #include "flash-rk27xx.h" + +#if CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_A #include "ftl-scheme-a.h" +#elif CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_B +#include "ftl-scheme-b.h" +#else +#error "NAND storage needs CONFIG_RK27XX_FTL in the target config" +#endif /* The boot ROM looks for ID blocks at every 512th raw sector of the boot * area, up to 50 positions, by metadata type 0x69. */ @@ -39,26 +47,49 @@ #define IDB_TYPE 0x69 #define IDB_MAX_BOOT_BLOCKS 64 +#define SECTORS_PER_MB 2048 + +#if CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_B +/* Scheme B's original firmware keeps its open exchange blocks to 8, and its + * mount recovers no more (ftl-scheme-b.h) */ +#define SCHEME_B_EXCH_BLOCKS 8 +#endif + +/* What ID block 1 records */ +struct idb_info +{ + uint32_t boot_blocks; /* raw blocks of the boot area */ + uint32_t sys_sectors; /* the SYS volume (code disk) */ + uint32_t data_sectors; /* Scheme B: the system data area after it */ +}; + static bool ftl_mounted = false; -/* The size of the SYS volume, from ID block 1. +#if CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_B +/* Scheme B's logical space holds the volumes back to back: SYS, the system + * data area, then USER to the end */ +static uint32_t vol_base[FTL_NUM_DRIVES]; +static uint32_t vol_size[FTL_NUM_DRIVES]; +#endif + +/* ID block 1, the sector after ID block 0: * - * ID block 0 is scrambled; ID block 1, the sector after it, is plain: - * - * +0 uint16_t LE blocks of bootloader + * +0 uint16_t LE raw blocks of the boot area * +2 uint16_t LE SYS volume size, MB + * +4 uint16_t LE system data area size, MB (Scheme B) * - * A sector marked 0x69 whose ID block 1 gives a sane block count and a SYS - * volume smaller than the chip is taken; descrambling ID block 0 to check its - * signature would buy little over that. Returns 0 if none is found. */ -static uint32_t idb_sys_sectors(void) + * ID block 0 is scrambled; ID block 1 is plain. A sector marked 0x69 whose + * ID block 1 gives a sane block count and a SYS volume smaller than the + * chip is taken; descrambling ID block 0 to check its signature would buy + * little over that. Returns false if none is found. */ +static bool idb_read(struct idb_info *idb) { const struct flash_geometry *geo = flash_get_geometry(); uint8_t data[FLASH_SECTOR_SIZE], meta[FLASH_META_SIZE]; - uint32_t sectors = 0; + bool found = false; uint32_t pos; - for (pos = 0; pos < IDB_POSITIONS && sectors == 0; pos++) + for (pos = 0; pos < IDB_POSITIONS && !found; pos++) { uint32_t raw = pos * IDB_STRIDE; uint32_t blocks, mb; @@ -82,17 +113,86 @@ static uint32_t idb_sys_sectors(void) mb = data[2] | (data[3] << 8); if (blocks > 0 && blocks <= IDB_MAX_BOOT_BLOCKS && mb > 0 && - mb * 2048 < geo->total_sectors) + mb * SECTORS_PER_MB < geo->total_sectors) { - sectors = mb * 2048; + idb->boot_blocks = blocks; + idb->sys_sectors = mb * SECTORS_PER_MB; + idb->data_sectors = (data[4] | (data[5] << 8)) * SECTORS_PER_MB; + found = true; } } - return sectors; + return found; } +#if CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_A +static uint32_t mount_scheme(const struct idb_info *idb) +{ + struct ftl_a_config config; + uint32_t ret = 0; + + config.sys_sectors = idb->sys_sectors; +#ifdef FTL_ALLOW_WRITE + config.read_only = false; + /* the rk2705 NAND bootloader's generation formats with flag 1; its + * write logic is the same as the standard one's */ + config.alt_format_flag = 1; + config.alt_format_writable = true; +#else + config.read_only = true; + config.alt_format_flag = 1; + config.alt_format_writable = false; +#endif + + if (ftl_a_mount(&config) != FTL_A_OK) + { + ret = 3; + } + else if (ftl_a_capacity(FTL_A_VOL_USER) == 0) + { + ret = 4; + } + return ret; +} +#else +static uint32_t mount_scheme(const struct idb_info *idb) +{ + const struct flash_geometry *geo = flash_get_geometry(); + struct ftl_b_config config; + uint32_t user_base = idb->sys_sectors + idb->data_sectors; + uint32_t ret = 0; + + config.first_block = (uint16_t)(idb->boot_blocks / geo->planes); + config.exch_blocks = SCHEME_B_EXCH_BLOCKS; +#ifdef FTL_ALLOW_WRITE + config.read_only = false; +#else + config.read_only = true; +#endif + + if (ftl_b_mount(&config) != FTL_B_OK) + { + ret = 3; + } + else if (ftl_b_capacity() <= user_base) + { + ret = 4; + } + else + { +#ifdef HAVE_RK27XX_NAND_SYS + vol_base[FTL_DRIVE_SYS] = 0; + vol_size[FTL_DRIVE_SYS] = idb->sys_sectors; +#endif + vol_base[FTL_DRIVE_USER] = user_base; + vol_size[FTL_DRIVE_USER] = ftl_b_capacity() - user_base; + } + return ret; +} +#endif + uint32_t ftl_init(void) { - struct ftl_a_config config; + struct idb_info idb; uint32_t ret = 0; flash_init(); @@ -101,41 +201,24 @@ uint32_t ftl_init(void) { ret = 1; } + else if (!idb_read(&idb)) + { + ret = 2; /* without it USER cannot be told from SYS */ + } else { - config.sys_sectors = idb_sys_sectors(); -#ifdef FTL_ALLOW_WRITE - config.read_only = false; - /* the rk2705 NAND bootloader's generation formats with flag 1; its - * write logic is the same as the standard one's */ - config.alt_format_flag = 1; - config.alt_format_writable = true; -#else - config.read_only = true; - config.alt_format_flag = 1; - config.alt_format_writable = false; -#endif - - if (config.sys_sectors == 0) - { - ret = 2; /* without it USER cannot be told from SYS */ - } - else if (ftl_a_mount(&config) != FTL_A_OK) - { - ret = 3; - } - else if (ftl_a_capacity(FTL_A_VOL_USER) == 0) - { - ret = 4; - } - else - { - ftl_mounted = true; - } + ret = mount_scheme(&idb); + ftl_mounted = ret == 0; } return ret; } +static bool drive_valid(int drive) +{ + return ftl_mounted && drive >= 0 && drive < FTL_NUM_DRIVES; +} + +#if CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_A /* The FTL volume behind a drive. SYS is reachable only when exposed. */ static int ftl_volume(int drive) { @@ -152,18 +235,77 @@ static int ftl_volume(int drive) return volume; } -static bool drive_valid(int drive) +static uint32_t drive_sectors(int drive) { - return ftl_mounted && drive >= 0 && drive < FTL_NUM_DRIVES; + return ftl_a_capacity(ftl_volume(drive)); } +static int drive_read(int drive, uint32_t sector, uint32_t count, void *buffer) +{ + return ftl_a_read(ftl_volume(drive), sector, buffer, count); +} + +#ifdef FTL_ALLOW_WRITE +static int drive_write(int drive, uint32_t sector, uint32_t count, + const void *buffer) +{ + return ftl_a_write(ftl_volume(drive), sector, buffer, count); +} +#endif + +static void drive_sync(void) +{ + ftl_a_sync(); +} +#else +static uint32_t drive_sectors(int drive) +{ + return vol_size[drive]; +} + +static bool in_volume(int drive, uint32_t sector, uint32_t count) +{ + return sector < vol_size[drive] && count <= vol_size[drive] - sector; +} + +static int drive_read(int drive, uint32_t sector, uint32_t count, void *buffer) +{ + int ret = 1; + + if (in_volume(drive, sector, count)) + { + ret = ftl_b_read(vol_base[drive] + sector, buffer, count); + } + return ret; +} + +#ifdef FTL_ALLOW_WRITE +static int drive_write(int drive, uint32_t sector, uint32_t count, + const void *buffer) +{ + int ret = 1; + + if (in_volume(drive, sector, count)) + { + ret = ftl_b_write(vol_base[drive] + sector, buffer, count); + } + return ret; +} +#endif + +static void drive_sync(void) +{ + ftl_b_sync(); +} +#endif + uint32_t ftl_get_sectors(int drive) { uint32_t sectors = 0; if (drive_valid(drive)) { - sectors = ftl_a_capacity(ftl_volume(drive)); + sectors = drive_sectors(drive); } return sectors; } @@ -174,7 +316,7 @@ uint32_t ftl_read(int drive, uint32_t sector, uint32_t count, void *buffer) if (drive_valid(drive)) { - ret = ftl_a_read(ftl_volume(drive), sector, buffer, count) ? 2 : 0; + ret = drive_read(drive, sector, count, buffer) ? 2 : 0; } return ret; } @@ -187,7 +329,7 @@ uint32_t ftl_write(int drive, uint32_t sector, uint32_t count, #ifdef FTL_ALLOW_WRITE if (drive_valid(drive)) { - ret = ftl_a_write(ftl_volume(drive), sector, buffer, count) ? 2 : 0; + ret = drive_write(drive, sector, count, buffer) ? 2 : 0; } #else /* refuse rather than pretend: a silent success would let the filesystem @@ -201,7 +343,7 @@ uint32_t ftl_sync(void) { if (ftl_mounted) { - ftl_a_sync(); + drive_sync(); } return 0; } From 01d18e42a9424cba43efa61d0dc4a06ec3575462 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 13:00:41 +0200 Subject: [PATCH 299/408] rk27xx: fix -Wundef warnings in the FTL scheme finder The debug menu tests CONFIG_NAND == NAND_RK27XX to include the FTL scheme finder. Most targets do not define CONFIG_NAND at all, so every native build but rk27xx's warned twice: "CONFIG_NAND" is not defined, evaluates to 0 [-Wundef] Test that it is defined first. Co-Authored-By: Claude Opus 5.5 Change-Id: I82a1e8fa2f09aac1c5f0d746fb6d475e389d12d6 --- apps/debug_menu.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/debug_menu.c b/apps/debug_menu.c index db8203643f..c408397aca 100644 --- a/apps/debug_menu.c +++ b/apps/debug_menu.c @@ -2857,7 +2857,8 @@ static bool view_ram_info(void) } #endif -#if (CONFIG_PLATFORM & PLATFORM_NATIVE) && (CONFIG_NAND == NAND_RK27XX) \ +#if (CONFIG_PLATFORM & PLATFORM_NATIVE) && defined(CONFIG_NAND) \ + && (CONFIG_NAND == NAND_RK27XX) \ && !(CONFIG_STORAGE & STORAGE_NAND) #include "ftl-probe-rk27xx.h" @@ -2961,7 +2962,8 @@ static const struct { #if ((CONFIG_PLATFORM & PLATFORM_NATIVE) || defined(SONY_NWZ_LINUX) || defined(HIBY_LINUX) || defined(FIIO_M3K_LINUX)) && !defined(SIMULATOR) { "View HW info", dbg_hw_info }, #endif -#if (CONFIG_PLATFORM & PLATFORM_NATIVE) && (CONFIG_NAND == NAND_RK27XX) \ +#if (CONFIG_PLATFORM & PLATFORM_NATIVE) && defined(CONFIG_NAND) \ + && (CONFIG_NAND == NAND_RK27XX) \ && !(CONFIG_STORAGE & STORAGE_NAND) { "View FTL scheme", dbg_ftl_scheme }, #endif From 3b142f45ced74fbb671d2e2073c03bfcbfac6376 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 16:40:13 +0200 Subject: [PATCH 300/408] rk27xx: fix a -Wundef warning in simulator builds config.h defines HAVE_STORAGE_FLUSH for the rk27xx Scheme A FTL when CONFIG_STORAGE has STORAGE_NAND and CONFIG_NAND is NAND_RK27XX. sim.h undefines CONFIG_NAND but keeps CONFIG_STORAGE, so a simulator for a NAND target - the iPod nano 2G - evaluated the undefined macro: "CONFIG_NAND" is not defined, evaluates to 0 [-Wundef] Test that it is defined first. Co-Authored-By: Claude Opus 5.5 Change-Id: Id1e328f08c14d7c5528d29f110fdbb8be68ab2f5 --- firmware/export/config.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/firmware/export/config.h b/firmware/export/config.h index 786ca1e3a8..7a206afb32 100644 --- a/firmware/export/config.h +++ b/firmware/export/config.h @@ -902,7 +902,8 @@ Lyre prototype 1 */ * pages in RAM (ftl-scheme-a.c) until a later write completes them; * storage_flush() commits them at shutdown, ROLO and wherever else it is * called. */ -#if (CONFIG_STORAGE & STORAGE_NAND) && (CONFIG_NAND == NAND_RK27XX) \ +#if (CONFIG_STORAGE & STORAGE_NAND) && defined(CONFIG_NAND) \ + && (CONFIG_NAND == NAND_RK27XX) \ && (CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_A) \ && !defined(HAVE_STORAGE_FLUSH) #define HAVE_STORAGE_FLUSH From c12f6ebacdfb64753c0f71b426829d468781275a Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 28 Sep 2026 23:18:09 -0400 Subject: [PATCH 301/408] imageviewer/jpeg: don't read past the end of entropy data The bit reader refilled from the input buffer without checking its end. The end-of-data check in the decode loops only runs once per MCU row, so a stream that desynchronises (or is truncated) read past the end of the file buffer for the rest of the row. Return zero bytes past the end instead; the pointer still advances so the per-row check stops the decode. Found with AddressSanitizer on a JPEG whose chroma is sampled more densely than its luma. Co-Authored-By: Claude Opus 5.5 Change-Id: Iaada1bc3cbf62b18f10fb2377c12d4d5cb9524de --- apps/plugins/imageviewer/jpeg/jpeg_decoder.c | 37 +++++++++++--------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c index 00acb94b95..700cdffcfe 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c +++ b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c @@ -1026,26 +1026,31 @@ void build_lut(struct jpeg* p_jpeg) * is evaluated multiple times. */ +/* Fetch the next entropy-coded byte. Past the end of the data this returns + * zero without reading, but still advances so the caller's end check stops + * the decode. */ +INLINE unsigned char fetch_byte(struct bitstream* pb) +{ + unsigned char byte = 0; + + if (pb->next_input_byte < pb->input_end) + { + byte = *pb->next_input_byte; + if (byte == 0xFF) /* legal marker can be byte stuffing or RSTm */ + { /* simplification: just skip the (one-byte) marker code */ + pb->next_input_byte++; + } + } + pb->next_input_byte++; + return byte; +} + INLINE void check_bit_buffer(struct bitstream* pb, int nbits) { if (pb->bits_left < nbits) { /* nbits is <= 16, so I can always refill 2 bytes in this case */ - unsigned char byte; - - byte = *pb->next_input_byte++; - if (byte == 0xFF) /* legal marker can be byte stuffing or RSTm */ - { /* simplification: just skip the (one-byte) marker code */ - pb->next_input_byte++; - } - pb->get_buffer = (pb->get_buffer << 8) | byte; - - byte = *pb->next_input_byte++; - if (byte == 0xFF) /* legal marker can be byte stuffing or RSTm */ - { /* simplification: just skip the (one-byte) marker code */ - pb->next_input_byte++; - } - pb->get_buffer = (pb->get_buffer << 8) | byte; - + pb->get_buffer = (pb->get_buffer << 8) | fetch_byte(pb); + pb->get_buffer = (pb->get_buffer << 8) | fetch_byte(pb); pb->bits_left += 16; } } From b897766a7addd87faa2a28748740301cb7ffc581 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 28 Sep 2026 23:19:50 -0400 Subject: [PATCH 302/408] jpeg: use the Huffman tables the scan header selects Both JPEG decoders ignored the DC/AC table selectors in the SOS header and always decoded luma with tables 0 and chroma with tables 1, the layout libjpeg writes by default. Files where all components share table 0, or where the slots are assigned differently, decoded to noise. Look up each component's tables from its selectors instead. Baseline JPEG only allows tables 0 and 1, which both decoders already hold, so this needs no extra memory; selectors above 1 are rejected. In the core loader tab_membership is no longer used and is removed. Co-Authored-By: Claude Opus 5.5 Change-Id: Ie1ec407ddecf1256fef410b1256b97af412fb194 --- apps/plugins/imageviewer/jpeg/jpeg_decoder.c | 17 ++++++++--- apps/plugins/imageviewer/jpeg/jpeg_decoder.h | 2 +- apps/recorder/jpeg_load.c | 32 ++++++-------------- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c index 700cdffcfe..2c529d2d2c 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c +++ b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c @@ -638,6 +638,11 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) p_jpeg->scanheader[i].ID = *p_src++; p_jpeg->scanheader[i].DC_select = *p_src >> 4; p_jpeg->scanheader[i].AC_select = *p_src++ & 0x0F; + if (p_jpeg->scanheader[i].DC_select > 1 + || p_jpeg->scanheader[i].AC_select > 1) + { + return (-5); /* Huffman table index out of range */ + } } p_src += 3; /* skip spectral information */ p_jpeg->p_entropy_data = p_src; @@ -1282,8 +1287,10 @@ int jpeg_decode(struct jpeg* p_jpeg, unsigned char* p_pixel[3], int s, r; /* huffman values */ int ci = p_jpeg->mcu_membership[blkn]; /* component index */ int ti = p_jpeg->tab_membership[blkn]; /* table index */ - struct derived_tbl* dctbl = &p_jpeg->dc_derived_tbls[ti]; - struct derived_tbl* actbl = &p_jpeg->ac_derived_tbls[ti]; + struct derived_tbl* dctbl = + &p_jpeg->dc_derived_tbls[p_jpeg->scanheader[ci].DC_select]; + struct derived_tbl* actbl = + &p_jpeg->ac_derived_tbls[p_jpeg->scanheader[ci].AC_select]; /* Section F.2.2.1: decode the DC coefficient difference */ s = huff_decode_dc(&bs, dctbl); @@ -1453,8 +1460,10 @@ int jpeg_decode(struct jpeg* p_jpeg, unsigned char* p_pixel[1], int downscale, int s, r; /* huffman values */ int ci = p_jpeg->mcu_membership[blkn]; /* component index */ int ti = p_jpeg->tab_membership[blkn]; /* table index */ - struct derived_tbl* dctbl = &p_jpeg->dc_derived_tbls[ti]; - struct derived_tbl* actbl = &p_jpeg->ac_derived_tbls[ti]; + struct derived_tbl* dctbl = + &p_jpeg->dc_derived_tbls[p_jpeg->scanheader[ci].DC_select]; + struct derived_tbl* actbl = + &p_jpeg->ac_derived_tbls[p_jpeg->scanheader[ci].AC_select]; /* Section F.2.2.1: decode the DC coefficient difference */ s = huff_decode_dc(&bs, dctbl); diff --git a/apps/plugins/imageviewer/jpeg/jpeg_decoder.h b/apps/plugins/imageviewer/jpeg/jpeg_decoder.h index 853c1ebbf5..90d99b27e7 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg_decoder.h +++ b/apps/plugins/imageviewer/jpeg/jpeg_decoder.h @@ -49,7 +49,7 @@ struct jpeg struct derived_tbl ac_derived_tbls[2]; struct frame_component frameheader[3]; /* Component descriptor */ - struct scan_component scanheader[3]; /* currently not used */ + struct scan_component scanheader[3]; /* Huffman tables per component */ int mcu_membership[6]; /* info per block */ int tab_membership[6]; diff --git a/apps/recorder/jpeg_load.c b/apps/recorder/jpeg_load.c index 6cf0a9640e..5f81c388b2 100644 --- a/apps/recorder/jpeg_load.c +++ b/apps/recorder/jpeg_load.c @@ -128,10 +128,9 @@ struct jpeg struct derived_tbl ac_derived_tbls[2]; struct frame_component frameheader[3]; /* Component descriptor */ - struct scan_component scanheader[3]; /* currently not used */ + struct scan_component scanheader[3]; /* Huffman tables per component */ int mcu_membership[6]; /* info per block */ - int tab_membership[6]; int subsample_x[3]; /* info per component */ int subsample_y[3]; bool resize; @@ -1171,6 +1170,11 @@ static int process_markers(struct jpeg* p_jpeg) >> 4; p_jpeg->scanheader[i].AC_select = c & 0x0F; marker_size -= 2; + if (p_jpeg->scanheader[i].DC_select > 1 + || p_jpeg->scanheader[i].AC_select > 1) + { + return (-5); /* Huffman table index out of range */ + } } /* skip spectral information */ e_skip_bytes(p_jpeg, marker_size); @@ -1472,10 +1476,6 @@ INLINE void fix_headers(struct jpeg* p_jpeg) p_jpeg->mcu_membership[1] = 0; p_jpeg->mcu_membership[2] = 1; p_jpeg->mcu_membership[3] = 2; - p_jpeg->tab_membership[0] = 0; /* DC, DC, AC, AC */ - p_jpeg->tab_membership[1] = 0; - p_jpeg->tab_membership[2] = 1; - p_jpeg->tab_membership[3] = 1; p_jpeg->subsample_x[0] = 1; p_jpeg->subsample_x[1] = 2; p_jpeg->subsample_x[2] = 2; @@ -1497,10 +1497,6 @@ INLINE void fix_headers(struct jpeg* p_jpeg) p_jpeg->mcu_membership[1] = 0; p_jpeg->mcu_membership[2] = 1; p_jpeg->mcu_membership[3] = 2; - p_jpeg->tab_membership[0] = 0; /* DC, DC, AC, AC */ - p_jpeg->tab_membership[1] = 0; - p_jpeg->tab_membership[2] = 1; - p_jpeg->tab_membership[3] = 1; p_jpeg->subsample_x[0] = 1; p_jpeg->subsample_x[1] = 1; p_jpeg->subsample_x[2] = 1; @@ -1522,12 +1518,6 @@ INLINE void fix_headers(struct jpeg* p_jpeg) p_jpeg->mcu_membership[3] = 0; p_jpeg->mcu_membership[4] = 1; p_jpeg->mcu_membership[5] = 2; - p_jpeg->tab_membership[0] = 0; - p_jpeg->tab_membership[1] = 0; - p_jpeg->tab_membership[2] = 0; - p_jpeg->tab_membership[3] = 0; - p_jpeg->tab_membership[4] = 1; - p_jpeg->tab_membership[5] = 1; p_jpeg->subsample_x[0] = 1; p_jpeg->subsample_x[1] = 2; p_jpeg->subsample_x[2] = 2; @@ -1546,9 +1536,6 @@ INLINE void fix_headers(struct jpeg* p_jpeg) p_jpeg->mcu_membership[0] = 0; p_jpeg->mcu_membership[1] = 1; p_jpeg->mcu_membership[2] = 2; - p_jpeg->tab_membership[0] = 0; - p_jpeg->tab_membership[1] = 1; - p_jpeg->tab_membership[2] = 1; p_jpeg->subsample_x[0] = 1; p_jpeg->subsample_x[1] = 1; p_jpeg->subsample_x[2] = 1; @@ -1845,14 +1832,15 @@ static struct img_part *store_row_jpeg(void *jpeg_args) for (blkn = 0; blkn < p_jpeg->blocks; blkn++) { int ci = p_jpeg->mcu_membership[blkn]; /* component index */ - int ti = p_jpeg->tab_membership[blkn]; /* table index */ #ifdef JPEG_IDCT_TRANSPOSE bool transpose = p_jpeg->v_scale[!!ci] > 2; #endif int k = 1; /* coefficient index */ int s, r; /* huffman values */ - struct derived_tbl* dctbl = &p_jpeg->dc_derived_tbls[ti]; - struct derived_tbl* actbl = &p_jpeg->ac_derived_tbls[ti]; + struct derived_tbl* dctbl = + &p_jpeg->dc_derived_tbls[p_jpeg->scanheader[ci].DC_select]; + struct derived_tbl* actbl = + &p_jpeg->ac_derived_tbls[p_jpeg->scanheader[ci].AC_select]; /* Section F.2.2.1: decode the DC coefficient difference */ huff_decode_dc(p_jpeg, dctbl, s, r); From 0e50ed3c439410308ede6cf1513352c5674f2222 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 28 Sep 2026 23:22:38 -0400 Subject: [PATCH 303/408] jpeg: use the quantization table each component selects Both JPEG decoders ignored the Tq selector in the frame header and always dequantized luma with table 0 and chroma with table 1. Files with a single shared table multiplied chroma by an empty table, and files with separate Cb and Cr tables used the wrong one for Cr. imageviewer/jpeg: build one dequantization table per component (3 instead of 2, +256 bytes) from the table it selects. tab_membership is no longer used and is removed. Core loader: the raw tables are pre-scaled in place for the IDCT, and luma and chroma can use different IDCT scales. fix_quant_tables() now maps each component to a table slot, copying a table that luma and chroma share at different scales to a slot no component uses (there are 4 slots and at most 3 components, so one is always free), and rewrites quanttable_select to that slot. No extra memory. Selectors above 3 are rejected. Co-Authored-By: Claude Opus 5.5 Change-Id: If04c61fb0fef95da11d98d9918ea7225d8a440b0 --- apps/plugins/imageviewer/jpeg/jpeg_decoder.c | 40 ++++--------- apps/plugins/imageviewer/jpeg/jpeg_decoder.h | 3 +- apps/recorder/jpeg_load.c | 63 ++++++++++++++++---- 3 files changed, 65 insertions(+), 41 deletions(-) diff --git a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c index 2c529d2d2c..21e7587c24 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c +++ b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c @@ -533,6 +533,8 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) p_jpeg->frameheader[i].horizontal_sampling = *p_src >> 4; p_jpeg->frameheader[i].vertical_sampling = *p_src++ & 0x0F; p_jpeg->frameheader[i].quanttable_select = *p_src++; + if (p_jpeg->frameheader[i].quanttable_select > 3) + return -8; /* Unsupported quantization table */ if (p_jpeg->frameheader[i].horizontal_sampling > 2 || p_jpeg->frameheader[i].vertical_sampling > 2) return -3; /* Unsupported SOF0 subsampling */ @@ -890,7 +892,7 @@ static const int zag[] = void build_lut(struct jpeg* p_jpeg) { - int i; + int i, c; fix_huff_tbl(p_jpeg->hufftable[0].huffmancodes_dc, &p_jpeg->dc_derived_tbls[0]); fix_huff_tbl(p_jpeg->hufftable[0].huffmancodes_ac, @@ -900,11 +902,14 @@ void build_lut(struct jpeg* p_jpeg) fix_huff_tbl(p_jpeg->hufftable[1].huffmancodes_ac, &p_jpeg->ac_derived_tbls[1]); - /* build the dequantization tables for the IDCT (De-ZiZagged) */ - for (i=0; i<64; i++) + /* build each component's dequantization table for the IDCT + (De-ZiZagged); p_jpeg->blocks is still the component count here */ + for (c=0; cblocks; c++) { - p_jpeg->qt_idct[0][zag[i]] = p_jpeg->quanttable[0][i]; - p_jpeg->qt_idct[1][zag[i]] = p_jpeg->quanttable[1][i]; + const int* qt = p_jpeg->quanttable[ + p_jpeg->frameheader[c].quanttable_select]; + for (i=0; i<64; i++) + p_jpeg->qt_idct[c][zag[i]] = qt[i]; } for (i=0; i<4; i++) @@ -923,10 +928,6 @@ void build_lut(struct jpeg* p_jpeg) p_jpeg->mcu_membership[1] = 0; p_jpeg->mcu_membership[2] = 1; p_jpeg->mcu_membership[3] = 2; - p_jpeg->tab_membership[0] = 0; /* DC, DC, AC, AC */ - p_jpeg->tab_membership[1] = 0; - p_jpeg->tab_membership[2] = 1; - p_jpeg->tab_membership[3] = 1; p_jpeg->subsample_x[0] = 1; p_jpeg->subsample_x[1] = 2; p_jpeg->subsample_x[2] = 2; @@ -948,10 +949,6 @@ void build_lut(struct jpeg* p_jpeg) p_jpeg->mcu_membership[1] = 0; p_jpeg->mcu_membership[2] = 1; p_jpeg->mcu_membership[3] = 2; - p_jpeg->tab_membership[0] = 0; /* DC, DC, AC, AC */ - p_jpeg->tab_membership[1] = 0; - p_jpeg->tab_membership[2] = 1; - p_jpeg->tab_membership[3] = 1; p_jpeg->subsample_x[0] = 1; p_jpeg->subsample_x[1] = 1; p_jpeg->subsample_x[2] = 1; @@ -973,12 +970,6 @@ void build_lut(struct jpeg* p_jpeg) p_jpeg->mcu_membership[3] = 0; p_jpeg->mcu_membership[4] = 1; p_jpeg->mcu_membership[5] = 2; - p_jpeg->tab_membership[0] = 0; - p_jpeg->tab_membership[1] = 0; - p_jpeg->tab_membership[2] = 0; - p_jpeg->tab_membership[3] = 0; - p_jpeg->tab_membership[4] = 1; - p_jpeg->tab_membership[5] = 1; p_jpeg->subsample_x[0] = 1; p_jpeg->subsample_x[1] = 2; p_jpeg->subsample_x[2] = 2; @@ -997,9 +988,6 @@ void build_lut(struct jpeg* p_jpeg) p_jpeg->mcu_membership[0] = 0; p_jpeg->mcu_membership[1] = 1; p_jpeg->mcu_membership[2] = 2; - p_jpeg->tab_membership[0] = 0; - p_jpeg->tab_membership[1] = 1; - p_jpeg->tab_membership[2] = 1; p_jpeg->subsample_x[0] = 1; p_jpeg->subsample_x[1] = 1; p_jpeg->subsample_x[2] = 1; @@ -1286,7 +1274,6 @@ int jpeg_decode(struct jpeg* p_jpeg, unsigned char* p_pixel[3], int k = 1; /* coefficient index */ int s, r; /* huffman values */ int ci = p_jpeg->mcu_membership[blkn]; /* component index */ - int ti = p_jpeg->tab_membership[blkn]; /* table index */ struct derived_tbl* dctbl = &p_jpeg->dc_derived_tbls[p_jpeg->scanheader[ci].DC_select]; struct derived_tbl* actbl = @@ -1349,11 +1336,11 @@ int jpeg_decode(struct jpeg* p_jpeg, unsigned char* p_pixel[3], if (ci == 0) { /* Y component needs to bother about block store */ pf_idct(p_byte[0]+store_offs[blkn], block, - p_jpeg->qt_idct[ti], skip_line[0]); + p_jpeg->qt_idct[ci], skip_line[0]); } else { /* chroma */ - pf_idct(p_byte[ci], block, p_jpeg->qt_idct[ti], + pf_idct(p_byte[ci], block, p_jpeg->qt_idct[ci], skip_line[ci]); } } /* for blkn */ @@ -1459,7 +1446,6 @@ int jpeg_decode(struct jpeg* p_jpeg, unsigned char* p_pixel[1], int downscale, int k = 1; /* coefficient index */ int s, r; /* huffman values */ int ci = p_jpeg->mcu_membership[blkn]; /* component index */ - int ti = p_jpeg->tab_membership[blkn]; /* table index */ struct derived_tbl* dctbl = &p_jpeg->dc_derived_tbls[p_jpeg->scanheader[ci].DC_select]; struct derived_tbl* actbl = @@ -1524,7 +1510,7 @@ int jpeg_decode(struct jpeg* p_jpeg, unsigned char* p_pixel[1], int downscale, if (ci == 0) { /* only for Y component */ - pf_idct(p_byte+store_offs[blkn], block, p_jpeg->qt_idct[ti], + pf_idct(p_byte+store_offs[blkn], block, p_jpeg->qt_idct[ci], skip_line); } } /* for blkn */ diff --git a/apps/plugins/imageviewer/jpeg/jpeg_decoder.h b/apps/plugins/imageviewer/jpeg/jpeg_decoder.h index 90d99b27e7..9262fcf7ff 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg_decoder.h +++ b/apps/plugins/imageviewer/jpeg/jpeg_decoder.h @@ -42,7 +42,7 @@ struct jpeg unsigned char* p_entropy_end; int quanttable[4][QUANT_TABLE_LENGTH]; /* raw quantization tables 0-3 */ - int qt_idct[2][QUANT_TABLE_LENGTH]; /* quantization tables for IDCT */ + int qt_idct[3][QUANT_TABLE_LENGTH]; /* per component, for IDCT */ struct huffman_table hufftable[2]; /* Huffman tables */ struct derived_tbl dc_derived_tbls[2]; /* Huffman-LUTs */ @@ -52,7 +52,6 @@ struct jpeg struct scan_component scanheader[3]; /* Huffman tables per component */ int mcu_membership[6]; /* info per block */ - int tab_membership[6]; int subsample_x[3]; /* info per component */ int subsample_y[3]; }; diff --git a/apps/recorder/jpeg_load.c b/apps/recorder/jpeg_load.c index 5f81c388b2..5f00ff7478 100644 --- a/apps/recorder/jpeg_load.c +++ b/apps/recorder/jpeg_load.c @@ -1043,6 +1043,8 @@ static int process_markers(struct jpeg* p_jpeg) p_jpeg->frameheader[i].vertical_sampling = c & 0x0F; p_jpeg->frameheader[i].quanttable_select = e_getc(p_jpeg, -1); + if (p_jpeg->frameheader[i].quanttable_select > 3) + return -8; /* Unsupported quantization table */ if (p_jpeg->frameheader[i].horizontal_sampling > 2 || p_jpeg->frameheader[i].vertical_sampling > 2) return -3; /* Unsupported SOF0 subsampling */ @@ -1567,23 +1569,60 @@ INLINE void fix_huff_tables(struct jpeg *p_jpeg) * quantization table when one of these IDCT routines is used, rather than * have the IDCT shift each value it processes. */ +/* Pre-scale the quantization tables for the IDCT. Luma and chroma can use + * different IDCT scales, so a table that both select is copied to a slot no + * component selects before scaling; each component's quanttable_select is + * rewritten to the slot it should use. There are 4 slots and at most 3 + * components, so when a table is shared a free slot always exists. */ INLINE void fix_quant_tables(struct jpeg *p_jpeg) { - int shift, i, j; + int want[4] = { -1, -1, -1, -1 }; /* scale shift wanted for each slot */ + int orig[4] = { 0, 1, 2, 3 }; /* table each slot holds */ + int c, c2, f, j; #ifdef HAVE_LCD_COLOR - const int k = 2; + const int nc = p_jpeg->blocks > 1 ? 3 : 1; #else - const int k = 1; + const int nc = 1; /* chroma is not decoded */ #endif - for (i = 0; i < k; i++) + for (c = 0; c < nc; c++) { - shift = idct_tbl[p_jpeg->v_scale[i]].scale; - if (shift) + int t = p_jpeg->frameheader[c].quanttable_select; + int shift = idct_tbl[p_jpeg->v_scale[!!c]].scale; + + for (f = 0; f < 4; f++) /* a slot already holding t at this scale? */ + if (orig[f] == t && want[f] == shift) + break; + if (f == 4 && want[t] < 0) + f = t; /* first use of t */ + if (f == 4) + { /* t is in use at another scale: copy it to a free slot that no + later component still needs */ + for (f = 0; f < 4; f++) + { + if (want[f] >= 0) + continue; + for (c2 = c + 1; c2 < nc; c2++) + if (p_jpeg->frameheader[c2].quanttable_select == f) + break; + if (c2 == nc) + break; + } + MEMCPY(p_jpeg->quanttable[f], p_jpeg->quanttable[t], + sizeof(p_jpeg->quanttable[f])); + orig[f] = t; + } + want[f] = shift; + p_jpeg->frameheader[c].quanttable_select = f; + } + + for (f = 0; f < 4; f++) + { + if (want[f] > 0) { for (j = 0; j < 64; j++) - p_jpeg->quanttable[i][j] <<= shift; + p_jpeg->quanttable[f][j] <<= want[f]; } } } @@ -1841,6 +1880,8 @@ static struct img_part *store_row_jpeg(void *jpeg_args) &p_jpeg->dc_derived_tbls[p_jpeg->scanheader[ci].DC_select]; struct derived_tbl* actbl = &p_jpeg->ac_derived_tbls[p_jpeg->scanheader[ci].AC_select]; + const int16_t *qt = p_jpeg->quanttable[ + p_jpeg->frameheader[ci].quanttable_select]; /* Section F.2.2.1: decode the DC coefficient difference */ huff_decode_dc(p_jpeg, dctbl, s, r); @@ -1853,13 +1894,11 @@ static struct img_part *store_row_jpeg(void *jpeg_args) #ifdef HAVE_LCD_COLOR p_jpeg->last_dc_val[ci] += s; /* output it (assumes zag[0] = 0) */ - block[0] = MULTIPLY16(p_jpeg->last_dc_val[ci], - p_jpeg->quanttable[!!ci][0]); + block[0] = MULTIPLY16(p_jpeg->last_dc_val[ci], qt[0]); #else p_jpeg->last_dc_val += s; /* output it (assumes zag[0] = 0) */ - block[0] = MULTIPLY16(p_jpeg->last_dc_val, - p_jpeg->quanttable[0][0]); + block[0] = MULTIPLY16(p_jpeg->last_dc_val, qt[0]); #endif /* coefficient buffer must be cleared */ MEMSET(block+1, 0, p_jpeg->zero_need[!!ci] * sizeof(int)); @@ -1877,7 +1916,7 @@ static struct img_part *store_row_jpeg(void *jpeg_args) goto skip_rest; r = get_bits(p_jpeg, s); r = HUFF_EXTEND(r, s); - r = MULTIPLY16(r, p_jpeg->quanttable[!!ci][k]); + r = MULTIPLY16(r, qt[k]); #ifdef JPEG_IDCT_TRANSPOSE block[zag[transpose ? k : k + 64]] = r ; #else From e32bfaadb16ae36a23a4a507b95f0fe94469a43c Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 28 Sep 2026 23:24:32 -0400 Subject: [PATCH 304/408] jpeg: reject layouts the decoders cannot handle Both JPEG decoders accepted several baseline layouts they cannot decode and produced garbage without an error: - chroma with sampling factors other than 1x1 (the MCU layout is chosen from luma alone, so any other chroma layout desynchronises) - files written as more than one scan, where the first scan does not hold every component (it was decoded as if it were interleaved) - scans whose components are not in frame order - a height of 0 in SOF, to be defined later by a DNL marker Reject these in process_markers(). The imageviewer then falls back to the jpegp decoder on colour targets, which handles all of them. Co-Authored-By: Claude Opus 5.5 Change-Id: Ia3373f2b934eef6e3f354b4d064faf2d89868050 --- apps/plugins/imageviewer/jpeg/jpeg_decoder.c | 17 ++++++++++++++++- apps/recorder/jpeg_load.c | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c index 21e7587c24..bc7e03f50e 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c +++ b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c @@ -540,6 +540,14 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) return -3; /* Unsupported SOF0 subsampling */ } p_jpeg->blocks = n; + for (i=1; iframeheader[i].horizontal_sampling != 1 + || p_jpeg->frameheader[i].vertical_sampling != 1) + return -3; /* Unsupported SOF0 subsampling */ + } + if (p_jpeg->x_size == 0 || p_jpeg->y_size == 0) + return -12; /* Height defined by DNL not supported */ /* A single-component scan is non-interleaved: the MCU is one 8x8 block regardless of the sampling factors (T.81 A.2.2) */ if (n == 1) @@ -631,13 +639,20 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) marker_size |= *p_src++; /* Lowbyte */ n = (marker_size-2-1-3)/2; - if (*p_src++ != n || (n != 1 && n != 3)) + if (*p_src++ != n || (n != 1 && n != 3) + /* one scan with all components; multi-scan files and + SOS before SOF are not supported (blocks = Nf here) */ + || n != p_jpeg->blocks) { return (-7); /* Unsupported SOS component specification */ } for (i=0; iscanheader[i].ID = *p_src++; + if (p_jpeg->scanheader[i].ID != p_jpeg->frameheader[i].ID) + { + return (-7); /* components out of frame order */ + } p_jpeg->scanheader[i].DC_select = *p_src >> 4; p_jpeg->scanheader[i].AC_select = *p_src++ & 0x0F; if (p_jpeg->scanheader[i].DC_select > 1 diff --git a/apps/recorder/jpeg_load.c b/apps/recorder/jpeg_load.c index 5f00ff7478..f6f43a32ee 100644 --- a/apps/recorder/jpeg_load.c +++ b/apps/recorder/jpeg_load.c @@ -1050,6 +1050,14 @@ static int process_markers(struct jpeg* p_jpeg) return -3; /* Unsupported SOF0 subsampling */ } p_jpeg->blocks = n; + for (i=1; iframeheader[i].horizontal_sampling != 1 + || p_jpeg->frameheader[i].vertical_sampling != 1) + return -3; /* Unsupported SOF0 subsampling */ + } + if (p_jpeg->x_size == 0 || p_jpeg->y_size == 0) + return -12; /* Height defined by DNL not supported */ /* A single-component scan is non-interleaved: the MCU is one 8x8 block regardless of the sampling factors (T.81 A.2.2) */ if (n == 1) @@ -1160,7 +1168,10 @@ static int process_markers(struct jpeg* p_jpeg) marker_size -= 2; n = (marker_size-1-3)/2; - if (e_getc(p_jpeg, -1) != n || (n != 1 && n != 3)) + if (e_getc(p_jpeg, -1) != n || (n != 1 && n != 3) + /* one scan with all components; multi-scan files and + SOS before SOF are not supported (blocks = Nf here) */ + || n != p_jpeg->blocks) { return (-7); /* Unsupported SOS component specification */ } @@ -1168,6 +1179,10 @@ static int process_markers(struct jpeg* p_jpeg) for (i=0; iscanheader[i].ID = e_getc(p_jpeg, -1); + if (p_jpeg->scanheader[i].ID != p_jpeg->frameheader[i].ID) + { + return (-7); /* components out of frame order */ + } p_jpeg->scanheader[i].DC_select = (c = e_getc(p_jpeg, -1)) >> 4; p_jpeg->scanheader[i].AC_select = c & 0x0F; From 5c74d20bcefe5d1d9ffea89eeddc5566b8e66c45 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 29 Sep 2026 08:27:12 -0400 Subject: [PATCH 305/408] jpeg_load: decode chroma sampled like 1x2 or 2x1 luma When all three components share 1x2 or 2x1 sampling there is no chroma subsampling, but each interleaved MCU holds two blocks per component. The core loader assumed one chroma block per MCU, so these files (the folder.jpg in the original report) decoded to garbage and have been rejected since chroma sampling is validated. Lay out the MCU generically in fix_headers(): each component's H x V blocks in turn, with a per-block position that places chroma blocks with the same offsets as luma. The MCU size and decode buffer now come from the luma sampling in colour builds too, and the chroma IDCT scale from the luma:chroma sampling ratio, which is unchanged for 1x1 chroma. The unused subsample_x/y fields are removed. All other layouts decode byte-identically to before at every scale. The new layouts decode byte-identically to the same image encoded as 4:4:4. Code size drops by 108 bytes on the e200 and struct jpeg by 20 bytes. Co-Authored-By: Claude Opus 5.5 Change-Id: I63a894d6609e56f3553a902afd6278ce70e04637 --- apps/recorder/jpeg_load.c | 155 ++++++++++++-------------------------- 1 file changed, 50 insertions(+), 105 deletions(-) diff --git a/apps/recorder/jpeg_load.c b/apps/recorder/jpeg_load.c index f6f43a32ee..eb10d1e94f 100644 --- a/apps/recorder/jpeg_load.c +++ b/apps/recorder/jpeg_load.c @@ -130,9 +130,8 @@ struct jpeg struct frame_component frameheader[3]; /* Component descriptor */ struct scan_component scanheader[3]; /* Huffman tables per component */ - int mcu_membership[6]; /* info per block */ - int subsample_x[3]; /* info per component */ - int subsample_y[3]; + int mcu_membership[6]; /* component of each block */ + unsigned char mcu_pos[6]; /* position of each block in its component */ bool resize; unsigned char buf[JPEG_READ_BUF_SIZE]; struct img_part part; @@ -1051,9 +1050,15 @@ static int process_markers(struct jpeg* p_jpeg) } p_jpeg->blocks = n; for (i=1; iframeheader[i].horizontal_sampling != 1 - || p_jpeg->frameheader[i].vertical_sampling != 1) + { /* chroma must be one block per MCU, or sampled like + luma when that is 1x2 or 2x1 (2x2 on all components + would exceed 10 blocks per MCU) */ + int h = p_jpeg->frameheader[i].horizontal_sampling; + int v = p_jpeg->frameheader[i].vertical_sampling; + if ((h != 1 || v != 1) + && (h != p_jpeg->frameheader[0].horizontal_sampling + || v != p_jpeg->frameheader[0].vertical_sampling + || h == v)) return -3; /* Unsupported SOF0 subsampling */ } if (p_jpeg->x_size == 0 || p_jpeg->y_size == 0) @@ -1475,96 +1480,39 @@ static const unsigned char zig[] = /* Reformat some image header data so that the decoder can use it properly. */ INLINE void fix_headers(struct jpeg* p_jpeg) { - int i; + int h = p_jpeg->frameheader[0].horizontal_sampling; + int v = p_jpeg->frameheader[0].vertical_sampling; + int ncomp = p_jpeg->blocks; /* still the component count here */ + int i, c, b; for (i=0; i<4; i++) p_jpeg->store_pos[i] = i; /* default ordering */ - - /* assignments for the decoding of blocks */ - if (p_jpeg->frameheader[0].horizontal_sampling == 2 - && p_jpeg->frameheader[0].vertical_sampling == 1) - { /* 4:2:2 */ - p_jpeg->blocks = 4; - p_jpeg->x_mbl = (p_jpeg->x_size+15) / 16; - p_jpeg->x_phys = p_jpeg->x_mbl * 16; - p_jpeg->y_mbl = (p_jpeg->y_size+7) / 8; - p_jpeg->y_phys = p_jpeg->y_mbl * 8; - p_jpeg->mcu_membership[0] = 0; /* Y1=Y2=0, U=1, V=2 */ - p_jpeg->mcu_membership[1] = 0; - p_jpeg->mcu_membership[2] = 1; - p_jpeg->mcu_membership[3] = 2; - p_jpeg->subsample_x[0] = 1; - p_jpeg->subsample_x[1] = 2; - p_jpeg->subsample_x[2] = 2; - p_jpeg->subsample_y[0] = 1; - p_jpeg->subsample_y[1] = 1; - p_jpeg->subsample_y[2] = 1; - } - else if (p_jpeg->frameheader[0].horizontal_sampling == 1 - && p_jpeg->frameheader[0].vertical_sampling == 2) - { /* 4:2:2 vertically subsampled */ - p_jpeg->store_pos[1] = 2; /* block positions are mirrored */ + if (h == 1 && v == 2) + { /* the second block of a component goes below the first */ + p_jpeg->store_pos[1] = 2; p_jpeg->store_pos[2] = 1; - p_jpeg->blocks = 4; - p_jpeg->x_mbl = (p_jpeg->x_size+7) / 8; - p_jpeg->x_phys = p_jpeg->x_mbl * 8; - p_jpeg->y_mbl = (p_jpeg->y_size+15) / 16; - p_jpeg->y_phys = p_jpeg->y_mbl * 16; - p_jpeg->mcu_membership[0] = 0; /* Y1=Y2=0, U=1, V=2 */ - p_jpeg->mcu_membership[1] = 0; - p_jpeg->mcu_membership[2] = 1; - p_jpeg->mcu_membership[3] = 2; - p_jpeg->subsample_x[0] = 1; - p_jpeg->subsample_x[1] = 1; - p_jpeg->subsample_x[2] = 1; - p_jpeg->subsample_y[0] = 1; - p_jpeg->subsample_y[1] = 2; - p_jpeg->subsample_y[2] = 2; - } - else if (p_jpeg->frameheader[0].horizontal_sampling == 2 - && p_jpeg->frameheader[0].vertical_sampling == 2) - { /* 4:2:0 */ - p_jpeg->blocks = 6; - p_jpeg->x_mbl = (p_jpeg->x_size+15) / 16; - p_jpeg->x_phys = p_jpeg->x_mbl * 16; - p_jpeg->y_mbl = (p_jpeg->y_size+15) / 16; - p_jpeg->y_phys = p_jpeg->y_mbl * 16; - p_jpeg->mcu_membership[0] = 0; - p_jpeg->mcu_membership[1] = 0; - p_jpeg->mcu_membership[2] = 0; - p_jpeg->mcu_membership[3] = 0; - p_jpeg->mcu_membership[4] = 1; - p_jpeg->mcu_membership[5] = 2; - p_jpeg->subsample_x[0] = 1; - p_jpeg->subsample_x[1] = 2; - p_jpeg->subsample_x[2] = 2; - p_jpeg->subsample_y[0] = 1; - p_jpeg->subsample_y[1] = 2; - p_jpeg->subsample_y[2] = 2; - } - else if (p_jpeg->frameheader[0].horizontal_sampling == 1 - && p_jpeg->frameheader[0].vertical_sampling == 1) - { /* 4:4:4 */ - /* don't overwrite p_jpeg->blocks */ - p_jpeg->x_mbl = (p_jpeg->x_size+7) / 8; - p_jpeg->x_phys = p_jpeg->x_mbl * 8; - p_jpeg->y_mbl = (p_jpeg->y_size+7) / 8; - p_jpeg->y_phys = p_jpeg->y_mbl * 8; - p_jpeg->mcu_membership[0] = 0; - p_jpeg->mcu_membership[1] = 1; - p_jpeg->mcu_membership[2] = 2; - p_jpeg->subsample_x[0] = 1; - p_jpeg->subsample_x[1] = 1; - p_jpeg->subsample_x[2] = 1; - p_jpeg->subsample_y[0] = 1; - p_jpeg->subsample_y[1] = 1; - p_jpeg->subsample_y[2] = 1; - } - else - { - /* error */ } + p_jpeg->x_mbl = (p_jpeg->x_size + 8*h - 1) / (8*h); + p_jpeg->x_phys = p_jpeg->x_mbl * 8*h; + p_jpeg->y_mbl = (p_jpeg->y_size + 8*v - 1) / (8*v); + p_jpeg->y_phys = p_jpeg->y_mbl * 8*v; + + /* Blocks in MCU order: each component's blocks in turn, H x V of them. + Chroma is either 1x1 or sampled like luma (checked in SOF), so an MCU + holds at most 6 blocks. */ + b = 0; + for (c = 0; c < ncomp; c++) + { + int n = p_jpeg->frameheader[c].horizontal_sampling + * p_jpeg->frameheader[c].vertical_sampling; + for (i = 0; i < n; i++, b++) + { + p_jpeg->mcu_membership[b] = c; + p_jpeg->mcu_pos[b] = i; + } + } + p_jpeg->blocks = b; } INLINE void fix_huff_tables(struct jpeg *p_jpeg) @@ -1852,15 +1800,10 @@ static void search_restart(struct jpeg *p_jpeg) static struct img_part *store_row_jpeg(void *jpeg_args) { struct jpeg *p_jpeg = (struct jpeg*) jpeg_args; -#ifdef HAVE_LCD_COLOR - int mcu_hscale = p_jpeg->h_scale[1]; - int mcu_vscale = p_jpeg->v_scale[1]; -#else int mcu_hscale = (p_jpeg->h_scale[0] + p_jpeg->frameheader[0].horizontal_sampling - 1); int mcu_vscale = (p_jpeg->v_scale[0] + p_jpeg->frameheader[0].vertical_sampling - 1); -#endif unsigned int width = p_jpeg->x_mbl << mcu_hscale; unsigned int b_width = width * JPEG_PIX_SZ; int height = BIT_N(mcu_vscale); @@ -1974,7 +1917,8 @@ block_end: { int idct_cols = BIT_N(MIN(p_jpeg->h_scale[!!ci], 3)); int idct_rows = BIT_N(p_jpeg->v_scale[!!ci]); - unsigned char *b_out = out + (ci ? ci : store_offs[blkn]); + unsigned char *b_out = out + ci + + store_offs[p_jpeg->mcu_pos[blkn]]; if (idct_tbl[p_jpeg->v_scale[!!ci]].v_idct) #ifdef JPEG_IDCT_TRANSPOSE idct_tbl[p_jpeg->v_scale[!!ci]].v_idct(block, @@ -2225,10 +2169,16 @@ int clip_jpeg_fd(int fd, int flags, (p_jpeg->y_size << p_jpeg->v_scale[0]) >> 3 == bm->height) resize = false; #ifdef HAVE_LCD_COLOR - p_jpeg->h_scale[1] = p_jpeg->h_scale[0] + - p_jpeg->frameheader[0].horizontal_sampling - 1; - p_jpeg->v_scale[1] = p_jpeg->v_scale[0] + - p_jpeg->frameheader[0].vertical_sampling - 1; + { /* chroma blocks are scaled up by the luma:chroma sampling ratio */ + struct frame_component *chroma = + &p_jpeg->frameheader[p_jpeg->blocks > 1 ? 1 : 0]; + p_jpeg->h_scale[1] = p_jpeg->h_scale[0] + + p_jpeg->frameheader[0].horizontal_sampling - + chroma->horizontal_sampling; + p_jpeg->v_scale[1] = p_jpeg->v_scale[0] + + p_jpeg->frameheader[0].vertical_sampling - + chroma->vertical_sampling; + } JDEBUGF("chroma IDCT size: %dx%d\n", BIT_N(p_jpeg->h_scale[1]), BIT_N(p_jpeg->v_scale[1])); #endif @@ -2279,15 +2229,10 @@ int clip_jpeg_fd(int fd, int flags, buf_start += sizeof(struct jpeg); #endif maxsize = buf_end - buf_start; -#ifdef HAVE_LCD_COLOR - int decode_buf_size = (p_jpeg->x_mbl << p_jpeg->h_scale[1]) - << p_jpeg->v_scale[1]; -#else int decode_buf_size = (p_jpeg->x_mbl << p_jpeg->h_scale[0]) << p_jpeg->v_scale[0]; decode_buf_size <<= p_jpeg->frameheader[0].horizontal_sampling + p_jpeg->frameheader[0].vertical_sampling - 2; -#endif decode_buf_size *= JPEG_PIX_SZ; JDEBUGF("decode buffer size: %d\n", decode_buf_size); if (return_size) From 2ce1cdcd8eb9e55fd0030d547ec486d6efafca7d Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 19:25:56 +0200 Subject: [PATCH 306/408] rk27xx: don't switch SD cards to high-speed mode Every SD write failed. The card took the write command and went to receive-data state, but never saw the block: CMD12 still found it there, and the controller reported a missing CRC status after each block, 20 retries over. Reads worked. Stress-tested over USB mass storage on a generic rk2705, with a SDHC card, in the normal firmware and in the bootloader alike. sd_init_card() switched the card to high-speed mode with CMD6. This host is an SD 1.01 controller with a card clock of at most 25 MHz, per the rk27xx datasheet; high speed and CMD6 came with SD 1.10, and a card switched to it evidently does not take the data this host drives. The original firmware never switches: after selecting the card it sets the block length and a 1-bit bus and stays at default speed. A slower card clock did not help; dropping the switch alone did. Leave the card at default speed. Tested on a generic rk2705: ums_stress.py over a 32 MiB window of the card - fill, verify, edge sizes, a mixed read/write soak - passes at 2.3 MB/s writing and reading, about 75% of what a 1-bit bus at 25 MHz carries. Co-Authored-By: Claude Opus 5.5 Change-Id: Icfafab0bb460c8c0fda3dea455df9a36622bf764 --- firmware/target/arm/rk27xx/sd-rk27xx.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/firmware/target/arm/rk27xx/sd-rk27xx.c b/firmware/target/arm/rk27xx/sd-rk27xx.c index 02180d9faa..36cea2068d 100644 --- a/firmware/target/arm/rk27xx/sd-rk27xx.c +++ b/firmware/target/arm/rk27xx/sd-rk27xx.c @@ -323,10 +323,11 @@ static int sd_init_card(void) if (!sd_wait_card_busy()) return -21; - /* CMD6 */ - if(!send_cmd(SD_SWITCH_FUNC, 0x80fffff1, RES_R1, &response)) - return -8; - sleep(HZ/10); + /* No CMD6 switch to high-speed mode: this is an SD 1.01 host, with a + * card clock of at most 25 MHz, and a card switched to high speed does + * not take the data it drives - every write failed, the card waiting in + * receive-data state for a block it never saw, while reads worked. The + * original firmware leaves the card at default speed too. */ /* Card back to full speed 25MHz*/ SD_CTRL = (SD_CTRL & ~0x7FF); From 17db971b66b1770cdc5e997e54b856a68d3aa77a Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 11:15:49 +0200 Subject: [PATCH 307/408] rk27xx: leave the SDRAM mode and timings as the boot left them Firmware builds reprogram the SDRAM controller twice. system_init() sets burst 8, T_RP = T_RCD = 1 and the refresh, with CAS 3 since a6538abd16 - CAS 2 on HM60X/HM801. And set_sdram_timing(), on every CPU frequency change, rewrites the mode: CAS 2 whenever the AHB runs at 100 MHz or below, which is every clock this code sets. On a generic rk2705 the firmware hangs in system_init() on those writes; with them skipped it boots. Doing the same writes from IRAM, so that nothing touches the SDRAM while the controller reprograms the chip, hangs the same way: it is the settings themselves. The board's SDRAM is an Elpida EDS1216AATA-75 (16 MB), a 133 MHz part at CAS 3 whose minimum clock period at CAS 2 is 10 ns - exactly the 100 MHz it runs at here, with no margin, next to minimal T_RP/T_RCD. On a Samsung YP-CP3 the firmware boots but corrupts memory at random once the clock first changes: data aborts on valid addresses in unrelated code, undefined instruction exceptions on valid instructions, glitches in the boot logo, crashes on USB plug and unplug. A memory test over 15 MB passes with the boot's setup (CAS 3, burst 1, T_RP = T_RCD = 2) and with system_init()'s values alike - it never changes the clock - and skipping only the system_init() writes is not enough, as set_sdram_timing() still selects CAS 2. With both removed the YP-CP3 runs, passes a USB mass storage stress test and survives USB unplug. The boot ROM's and the bootloaders' setup works on every rk27xx target seen, and nothing before Rockbox changes it (the NAND bootloader's stage 1 and rk27load's s1 only probe the organisation). The gain claimed for the tweak was a slight improvement in memory throughput. So remove it for every rk27xx target, the HM60X/HM801 CAS 2 included, and have set_sdram_timing() adjust only the refresh to the bus clock. Co-Authored-By: Claude Opus 5.5 Change-Id: Icd063367783b0ae6eb80bad67d908269c07e704f --- firmware/target/arm/rk27xx/system-rk27xx.c | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/firmware/target/arm/rk27xx/system-rk27xx.c b/firmware/target/arm/rk27xx/system-rk27xx.c index 6cbc3aa9ae..729b0c25a6 100644 --- a/firmware/target/arm/rk27xx/system-rk27xx.c +++ b/firmware/target/arm/rk27xx/system-rk27xx.c @@ -116,19 +116,6 @@ void system_init(void) WDTCON &= ~(1<<3); #ifndef BOOTLOADER - /* SDRAM tweaks. Note this assumes 100MHz AHB+SDRAM clock. */ - -#if !(defined(HM60X) || defined(HM801)) - MCSDR_MODE = (3<<4)|3; /* CAS=3, burst=8(2^3) -- Safe but slower */ -#else - MCSDR_MODE = (2<<4)|3; /* CAS=2, burst=8(2^3) -- Ideal but causes startup issues on (some?) IHIFI devices */ -#endif - - MCSDR_T_REF = (125*100) >> 3; /* 125/8 = 15.625 autorefresh interval */ - MCSDR_T_RFC = (64*100) / 1000; /* autorefresh period */ - MCSDR_T_RP = 1; /* precharge period */ - MCSDR_T_RCD = 1; /* active to RD/WR delay */ - /* turn off clock for unused modules - not the NAND controller's: the * FTL uses it, and a register access with its HCLK gated aborts */ SCU_CLKCFG |= CLKCFG_WDT | /* WDT pclk */ @@ -245,12 +232,8 @@ void commit_discard_dcache_range (const void *base, unsigned int size) #if !defined(BOOTLOADER) && defined(HAVE_ADJUSTABLE_CPU_FREQ) static inline void set_sdram_timing(int ahb_freq) { -#if 1 - if (ahb_freq > 100000000) - MCSDR_MODE = (3<<4)|3; /* CAS=3, burst=8(2^3) */ - else - MCSDR_MODE = (2<<4)|3; /* CAS=2, burst=8(2^3) */ -#endif + /* refresh only: the mode register - CAS latency - stays as the boot set + * it (see system_init()) */ MCSDR_T_REF = (125*ahb_freq/1000000) >> 3; MCSDR_T_RFC = (64*ahb_freq/1000000)/1000; } From 7e9d621f29720df8bfddab5861e2e267c13cf65c Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 21:00:02 +0200 Subject: [PATCH 308/408] rk27xx: read each run of NAND sectors with one page latch flash_read() latched the page afresh for every sector. The controller streams a page from its first sector, so each sector also cost a transfer of every sector before it in the page: reading an 8-sector page sector by sector took 8 array loads and 36 sector transfers instead of 1 and 8. Over USB mass storage on a Samsung YP-CP3 the NAND read at 1.87 MB/s, against 8.25 MB/s in the original firmware. Read each run of sectors that lie consecutively in one raw page with a single latch. On a two-plane part a run of FTL sectors stays in one page until it moves on to the other plane. On the YP-CP3, same test: 5.64 MB/s, every read verified. Co-Authored-By: Claude Opus 5.5 Change-Id: I9b08e56f4429ae432ccd2d2e23b9b34c18e56e97 --- firmware/target/arm/rk27xx/flash-rk27xx.c | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.c b/firmware/target/arm/rk27xx/flash-rk27xx.c index f3a9ab78b1..0f84e9c9ae 100644 --- a/firmware/target/arm/rk27xx/flash-rk27xx.c +++ b/firmware/target/arm/rk27xx/flash-rk27xx.c @@ -331,11 +331,18 @@ int flash_read(uint32_t sec, void *data, void *meta, unsigned n) unsigned i; int ret = ready ? 0 : 1; - /* One page latch per sector. Reading whole runs in one pass is an - * obvious speed-up, left until the access pattern has been measured. */ - for (i = 0; i < n && ready; i++) + unsigned run; + + /* One page latch per run of sectors that lie consecutively in one raw + * page. Latching per sector instead cost an array load per sector and, + * the controller streaming a page from its first sector, a transfer of + * every sector before it: 8 loads and 36 transfers for an 8-sector page + * read sector by sector, against 1 and 8. On a two-plane part a run of + * FTL sectors stays in one page until it moves on to the other plane. */ + for (i = 0; i < n && ready; i += run) { uint32_t raw = sec_to_raw(sec + i); + uint32_t slot = raw % geo.sec_per_page_raw; if (raw >= geo.total_sectors) { @@ -343,8 +350,14 @@ int flash_read(uint32_t sec, void *data, void *meta, unsigned n) break; } - if (read_raw_run(raw / geo.sec_per_page_raw, - raw % geo.sec_per_page_raw, 1, + run = 1; + while (i + run < n && slot + run < geo.sec_per_page_raw && + sec_to_raw(sec + i + run) == raw + run) + { + run++; + } + + if (read_raw_run(raw / geo.sec_per_page_raw, slot, run, d ? d + (size_t)i * FLASH_SECTOR_SIZE : NULL, m ? m + (size_t)i * FLASH_META_SIZE : NULL, ecc_mode)) { From cc7d8d07b282d76716a3c8982dd95bb0d0644ffb Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 21:19:02 +0200 Subject: [PATCH 309/408] rk27xx: program both planes of a NAND page at once On a two-plane part an FTL page spans the same page of two blocks, one in each plane, and flash_program() programmed them one after the other: two program busy times (tPROG) per page. The original firmware's FlashProgEnhanced() programs both with one two-plane program - 80h, the first page, 11h, a wait of tDBSY, 81h, the second page, 10h - so the planes share one tPROG. Do the same where the runs of a write cover the same page of both planes, on parts that take 81h for the second page. The original firmware sends 80h there on Toshiba and Micron parts, which address the planes differently too; those still program a page at a time. enum vendor_t moves to nand-target.h for the check. Over USB mass storage on a Samsung YP-CP3 the NAND wrote at 2.22 MB/s, against 4.35 MB/s in the original firmware; now 2.86 MB/s, every read verified. Co-Authored-By: Claude Opus 5.5 Change-Id: I2f40e2ec426cf787b4ce452cbd0d0eb4b702e168 --- firmware/target/arm/rk27xx/flash-rk27xx.c | 141 ++++++++++++++++------ firmware/target/arm/rk27xx/nand-rk27xx.c | 10 -- firmware/target/arm/rk27xx/nand-target.h | 13 +- 3 files changed, 118 insertions(+), 46 deletions(-) diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.c b/firmware/target/arm/rk27xx/flash-rk27xx.c index 0f84e9c9ae..50d457a665 100644 --- a/firmware/target/arm/rk27xx/flash-rk27xx.c +++ b/firmware/target/arm/rk27xx/flash-rk27xx.c @@ -52,6 +52,9 @@ #define CMD_READ_2ND 0x30 #define CMD_PROG_1ST 0x80 #define CMD_PROG_2ND 0x10 +#define CMD_PROG_PLANE 0x11 /* end of a two-plane program's first + * page */ +#define CMD_PROG_1ST_P1 0x81 /* start of its second page */ #define CMD_ERASE_1ST 0x60 #define CMD_ERASE_2ND 0xD0 #define CMD_STATUS 0x70 @@ -72,11 +75,14 @@ /* Busy limits: the datasheet maxima (tPROG 2.2 ms, tBERS 10 ms) with margin */ #define PROG_TIMEOUT_US 5000 #define ERASE_TIMEOUT_US 20000 +/* ...and tDBSY, the busy time after CMD_PROG_PLANE: 1 us at most */ +#define PLANE_TIMEOUT_US 10 static struct flash_geometry geo; static bool ready; static bool writable; static bool meta_passthrough; +static bool two_plane_prog; static uint32_t ecc_mode; /* BCHCTL mode bits: 0 or BCH_T14 */ static uint32_t boot_area = UINT32_MAX; static struct flash_stats stats; @@ -111,6 +117,13 @@ int flash_layer_init(void) geo.sec_per_block = f->sec_per_block; geo.total_blocks = f->total_bloks; geo.total_sectors = f->total_phy_sec; + /* The OF programs both planes' pages at once on every two-plane + * part, its second page with 0x81 - or 0x80 on Toshiba and Micron + * parts, whose plane addressing differs too. Only the 0x81 parts + * (a Samsung one tested) are done here; the others program a page + * at a time. */ + two_plane_prog = f->mul_plane == 2 && + f->vendor != TOSHIBA && f->vendor != MICRON; ready = true; } return ret; @@ -411,32 +424,37 @@ static void put_words(uintptr_t dst, const uint8_t *src, uint32_t len) } } -/* Program `n` sectors of raw page `row`, from sector `first` in it. */ -static int prog_raw_run(uint32_t row, uint32_t first, uint32_t n, - const uint8_t *data, const uint8_t *meta) +/* `n` sectors of raw page `row`, from sector `first` in it */ +struct prog_run { - uint32_t col = first * SECTOR_STRIDE; + uint32_t row; + uint32_t first; + uint32_t n; + const uint8_t *data; + const uint8_t *meta; +}; + +/* Send `cmd` and the address of `r`, and load its sectors into the chip's + * page register. */ +static void load_run(uint8_t cmd, const struct prog_run *r) +{ + uint32_t col = r->first * SECTOR_STRIDE; uint32_t i; - int fail; - flash_chip_select(0); - FMCTL |= FM_PROTECT; /* lift WP# for this operation */ - wait_flash_ready(); - - FLASH_CMD(0) = CMD_PROG_1ST; + FLASH_CMD(0) = cmd; FLASH_ADDR(0) = col & 0xff; FLASH_ADDR(0) = (col >> 8) & 0xff; - send_row(row); + send_row(r->row); - for (i = 0; i < n; i++) + for (i = 0; i < r->n; i++) { uint32_t buf = i & 3; uint8_t spare[SPARE_SIZE]; memset(spare, 0xff, sizeof(spare)); - if (meta) + if (r->meta) { - memcpy(spare, meta + i * FLASH_META_SIZE, FLASH_META_SIZE); + memcpy(spare, r->meta + i * FLASH_META_SIZE, FLASH_META_SIZE); } if (!meta_passthrough) { @@ -450,7 +468,7 @@ static int prog_raw_run(uint32_t row, uint32_t first, uint32_t n, } put_words((uintptr_t)&PAGE_BUF + (buf << 9), - data ? data + (size_t)i * FLASH_SECTOR_SIZE : NULL, + r->data ? r->data + (size_t)i * FLASH_SECTOR_SIZE : NULL, FLASH_SECTOR_SIZE); put_words((uintptr_t)&SPARE_BUF + (buf << 4), spare, SPARE_SIZE); @@ -461,9 +479,38 @@ static int prog_raw_run(uint32_t row, uint32_t first, uint32_t n, while (!(FLCTL & FL_RDY)) { } +} - FLASH_CMD(0) = CMD_PROG_2ND; - fail = wait_ready_us(PROG_TIMEOUT_US) || (read_status() & NAND_STATUS_FAIL); +/* Program one raw page, or with `planes` 2 a page in each plane - r[0] in + * the first, r[1] the same page of the second - with one two-plane program + * as the OF's FlashProgEnhanced() does: 80h, the first page, 11h, tDBSY, + * 81h, the second page, 10h. The planes then share one tPROG. */ +static int prog_raw(const struct prog_run *r, unsigned planes) +{ + int fail; + + flash_chip_select(0); + FMCTL |= FM_PROTECT; /* lift WP# for this operation */ + wait_flash_ready(); + + load_run(CMD_PROG_1ST, &r[0]); + fail = 0; + if (planes == 2) + { + FLASH_CMD(0) = CMD_PROG_PLANE; + fail = wait_ready_us(PLANE_TIMEOUT_US); + if (!fail) + { + load_run(CMD_PROG_1ST_P1, &r[1]); + } + } + + if (!fail) + { + FLASH_CMD(0) = CMD_PROG_2ND; + fail = wait_ready_us(PROG_TIMEOUT_US) + || (read_status() & NAND_STATUS_FAIL); + } flash_chip_deselect(); FMCTL &= ~FM_PROTECT; @@ -500,12 +547,38 @@ static int erase_raw_block(uint32_t row) return fail ? 1 : 0; } -/* Sectors of the FTL's view are split into one program per raw page - and - * so per plane. */ +/* Fill `r` with the run of sectors from sec + i, up to sec + n, that lie + * consecutively in one raw page. Returns its length, or 0 past the chip. */ +static uint32_t get_run(uint32_t sec, uint32_t i, uint32_t n, + const uint8_t *data, const uint8_t *meta, + struct prog_run *r) +{ + uint32_t raw = sec_to_raw(sec + i); + uint32_t run = 1; + + if (raw >= geo.total_sectors) + { + return 0; + } + + r->row = raw / geo.sec_per_page_raw; + r->first = raw % geo.sec_per_page_raw; + while (i + run < n && r->first + run < geo.sec_per_page_raw && + sec_to_raw(sec + i + run) == raw + run) + { + run++; + } + r->n = run; + r->data = data ? data + (size_t)i * FLASH_SECTOR_SIZE : NULL; + r->meta = meta ? meta + (size_t)i * FLASH_META_SIZE : NULL; + return run; +} + +/* Sectors of the FTL's view are split into one program per raw page, or + * where they cover the same page of both planes, one per pair of pages. */ int flash_program(uint32_t sec, const void *data, const void *meta, unsigned n) { - const uint8_t *d = data; - const uint8_t *m = meta; + uint32_t ppb_raw = geo.sec_per_block_raw / geo.sec_per_page_raw; uint32_t i = 0; int ret = 0; @@ -525,32 +598,30 @@ int flash_program(uint32_t sec, const void *data, const void *meta, unsigned n) { while (i < n && ret == 0) { - uint32_t raw = sec_to_raw(sec + i); - uint32_t row = raw / geo.sec_per_page_raw; - uint32_t first = raw % geo.sec_per_page_raw; - uint32_t run = 1; + struct prog_run r[2]; + uint32_t len = get_run(sec, i, n, data, meta, &r[0]); + unsigned planes = 1; - if (raw >= geo.total_sectors) + if (len == 0) { ret = 1; break; } - /* extend the run while the next sector is the next slot of the - * same raw page */ - while (i + run < n && first + run < geo.sec_per_page_raw && - sec_to_raw(sec + i + run) == raw + run) + /* sec_to_raw() puts a page's second plane in the next block */ + if (two_plane_prog && i + len < n && + get_run(sec, i + len, n, data, meta, &r[1]) != 0 && + r[1].row == r[0].row + ppb_raw) { - run++; + len += r[1].n; + planes = 2; } - if (prog_raw_run(row, first, run, - d ? d + (size_t)i * FLASH_SECTOR_SIZE : NULL, - m ? m + (size_t)i * FLASH_META_SIZE : NULL)) + if (prog_raw(r, planes)) { ret = 1; } - i += run; + i += len; } } return ret; diff --git a/firmware/target/arm/rk27xx/nand-rk27xx.c b/firmware/target/arm/rk27xx/nand-rk27xx.c index 0d0f7d9b02..2cc1a348f8 100644 --- a/firmware/target/arm/rk27xx/nand-rk27xx.c +++ b/firmware/target/arm/rk27xx/nand-rk27xx.c @@ -48,16 +48,6 @@ struct flashspec_t flash_spec[MAX_FLASH_NUM]; /* sum of all phy sectors in all chips */ uint32_t total_phy_sec; -enum vendor_t { - SAMSUNG, - TOSHIBA, - HYNIX, - INFINEON, - MICRON, - RENESAS, - ST -}; - /* taken from OF - one entry per device_info[] row. The OF's table is * 76 79 f1 da dc d3 d5 d7; 0xd5 was once missing here, which shifted every * later code onto the capacity one row up and sized a 4 GiB 0xd7 part as diff --git a/firmware/target/arm/rk27xx/nand-target.h b/firmware/target/arm/rk27xx/nand-target.h index 53fdc61647..9663e467da 100644 --- a/firmware/target/arm/rk27xx/nand-target.h +++ b/firmware/target/arm/rk27xx/nand-target.h @@ -26,6 +26,17 @@ #define MAX_FLASH_NUM 4 +/* flashspec_t.vendor: the index of the maker ID in manufacture_id_tbl[] */ +enum vendor_t { + SAMSUNG, + TOSHIBA, + HYNIX, + INFINEON, + MICRON, + RENESAS, + ST +}; + /* Per-chip geometry, as the OF derives it from the READ_ID response. * * The "_raw" fields describe one physical plane; the others are the @@ -41,7 +52,7 @@ struct flashspec_t uint8_t large; uint8_t five; uint8_t mlc; - uint8_t vendor; + uint8_t vendor; /* enum vendor_t */ uint8_t access_time; uint8_t sec_per_page; uint8_t sec_per_page_raw; From ae6c5eb8a86f23287d7ad94f2ec643080733ce09 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 21:36:03 +0200 Subject: [PATCH 310/408] rk27xx: overlap NAND write staging with the transfer Every sector of a write waited for the previous sector's transfer to the chip before it was copied into a controller slot, so the copy and the transfer never ran together. The slot the copy goes to is not the one in transfer: copy first and wait only before the BCH engine and the transfer restart. On the Samsung YP-CP3, same test: 3.26 MB/s, every read verified, also after a power cycle. Co-Authored-By: Claude Opus 5.5 Change-Id: I41f43658916891a2ded74bbe21b6970f7cc78068 --- firmware/target/arm/rk27xx/flash-rk27xx.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.c b/firmware/target/arm/rk27xx/flash-rk27xx.c index 50d457a665..0478f97bfe 100644 --- a/firmware/target/arm/rk27xx/flash-rk27xx.c +++ b/firmware/target/arm/rk27xx/flash-rk27xx.c @@ -461,17 +461,17 @@ static void load_run(uint8_t cmd, const struct prog_run *r) spare[META_PROGRAMMED] = 0x00; } - /* a slot is reused every four sectors: its last transfer must be - * done */ - while (!(FLCTL & FL_RDY)) - { - } - + /* stage this sector while the previous one, in another slot, + * transfers; the BCH engine restarts only once that is done */ put_words((uintptr_t)&PAGE_BUF + (buf << 9), r->data ? r->data + (size_t)i * FLASH_SECTOR_SIZE : NULL, FLASH_SECTOR_SIZE); put_words((uintptr_t)&SPARE_BUF + (buf << 4), spare, SPARE_SIZE); + while (!(FLCTL & FL_RDY)) + { + } + BCHCTL = BCH_WR | BCH_RST; FLCTL = FL_KICK_WRITE | (buf << 3); } From bfab6a2276ba2aac86d3c7273fd7940f8fcb0cdf Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Thu, 1 Oct 2026 15:51:34 -0400 Subject: [PATCH 311/408] Translation Updates * German (Wilfried Winkler) * Italian (Alessio Lenzi) Change-Id: I85b7936c85f188c09dc3621133ae83d1b70f6187 --- apps/lang/deutsch.lang | 12 ++++++------ apps/lang/italiano.lang | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/lang/deutsch.lang b/apps/lang/deutsch.lang index 9e6cebca59..4ded6c623f 100644 --- a/apps/lang/deutsch.lang +++ b/apps/lang/deutsch.lang @@ -9092,15 +9092,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Tiefengrenzfrequenz" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Tiefengrenzfrequenz" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Tiefengrenzfrequenz" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Tiefengrenzfrequenz" @@ -9109,15 +9109,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Höhengrenzfrequenz" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Höhengrenzfrequenz" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Höhengrenzfrequenz" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Höhengrenzfrequenz" diff --git a/apps/lang/italiano.lang b/apps/lang/italiano.lang index 8dd4d21165..ebcff7b644 100644 --- a/apps/lang/italiano.lang +++ b/apps/lang/italiano.lang @@ -9088,15 +9088,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Taglio Bassi" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Taglio Bassi" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Taglio Bassi" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Taglio Bassi" @@ -9105,15 +9105,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Taglio Alti" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Taglio Alti" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Taglio Alti" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Taglio Alti" From 9605950788b0bf87c0914a6af5c70dbbb3768260 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 21:48:05 +0200 Subject: [PATCH 312/408] rk27xx: re-enable the UDC interrupt at every connect usb_drv_exit() masks the UDC interrupt in the interrupt controller at every disconnect, but only usb_init_device(), once at boot, unmasked it. After the first unplug the UDC raised no more interrupts: on the next plug the charging icon showed - plug detection polls VBUS_STS - but the host's reset and requests went unanswered, so the device never enumerated and the USB screen never came up. Unmask it in usb_drv_init(), which runs at every connect, so that it pairs with the mask in usb_drv_exit(). The interrupt is now masked while USB is off, at boot too; nothing needs it then. Tested on a Samsung YP-CP3: it enumerates at every replug. Co-Authored-By: Claude Opus 5.5 Change-Id: Iab7b7e93c140990c39b0e6dfae23ffcc829f6ab2 --- firmware/target/arm/rk27xx/usb-drv-rk27xx.c | 5 +++++ firmware/target/arm/rk27xx/usb-rk27xx.c | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/firmware/target/arm/rk27xx/usb-drv-rk27xx.c b/firmware/target/arm/rk27xx/usb-drv-rk27xx.c index 33a1650fea..0f7637ebf9 100644 --- a/firmware/target/arm/rk27xx/usb-drv-rk27xx.c +++ b/firmware/target/arm/rk27xx/usb-drv-rk27xx.c @@ -687,6 +687,11 @@ void usb_drv_init(void) { udc_reset_semaphores(); tick_add_task(udc_tick); + + /* enable USB interrupts in interrupt controller - here, as + * usb_drv_exit() disables them at every disconnect */ + INTC_IMR |= IRQ_ARM_UDC; + INTC_IECR |= IRQ_ARM_UDC; } /* Present ourselves to the host. Called by usb_enable() once usb_core_init() diff --git a/firmware/target/arm/rk27xx/usb-rk27xx.c b/firmware/target/arm/rk27xx/usb-rk27xx.c index 69f5bb195b..6ac04e41b5 100644 --- a/firmware/target/arm/rk27xx/usb-rk27xx.c +++ b/firmware/target/arm/rk27xx/usb-rk27xx.c @@ -31,10 +31,6 @@ int usb_status = USB_EXTRACTED; void usb_init_device(void) { - /* enable UDC interrupt */ - INTC_IMR |= (1<<16); - INTC_IECR |= (1<<16); - EN_INT = EN_SUSP_INTR | /* Enable Suspend Interrupt */ EN_RESUME_INTR | /* Enable Resume Interrupt */ EN_USBRST_INTR | /* Enable USB Reset Interrupt */ From e28511a757fffc2607c98fbeddf8ca04748ff163 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Tue, 29 Sep 2026 22:58:46 +0300 Subject: [PATCH 313/408] rbutilqt: Fix case of "iPod" in logs and translations Functions, classes and variables not modified. Change-Id: I252c32c5c22294d73896e6ad6368328f260bdcab --- utils/rbutilqt/base/autodetection.cpp | 4 +-- utils/rbutilqt/base/bootloaderinstallipod.cpp | 10 +++--- .../rbutilqt/base/bootloaderinstallsansa.cpp | 2 +- utils/rbutilqt/changelog.txt | 2 +- utils/rbutilqt/configure.cpp | 2 +- utils/rbutilqt/lang/rbutil_cs.ts | 20 +++++------ utils/rbutilqt/lang/rbutil_de.ts | 20 +++++------ utils/rbutilqt/lang/rbutil_fi.ts | 10 +++--- utils/rbutilqt/lang/rbutil_fr.ts | 20 +++++------ utils/rbutilqt/lang/rbutil_gr.ts | 14 ++++---- utils/rbutilqt/lang/rbutil_he.ts | 10 +++--- utils/rbutilqt/lang/rbutil_it.ts | 16 ++++----- utils/rbutilqt/lang/rbutil_ja.ts | 10 +++--- utils/rbutilqt/lang/rbutil_ko.ts | 16 ++++----- utils/rbutilqt/lang/rbutil_nl.ts | 20 +++++------ utils/rbutilqt/lang/rbutil_pl.ts | 12 +++---- utils/rbutilqt/lang/rbutil_pt.ts | 14 ++++---- utils/rbutilqt/lang/rbutil_pt_BR.ts | 20 +++++------ utils/rbutilqt/lang/rbutil_ru.ts | 10 +++--- utils/rbutilqt/lang/rbutil_tr.ts | 20 +++++------ utils/rbutilqt/lang/rbutil_zh_CN.ts | 10 +++--- utils/rbutilqt/lang/rbutil_zh_TW.ts | 10 +++--- utils/rbutilqt/rbutil.ini | 34 +++++++++---------- 23 files changed, 153 insertions(+), 153 deletions(-) diff --git a/utils/rbutilqt/base/autodetection.cpp b/utils/rbutilqt/base/autodetection.cpp index 820805ac69..d49fe03828 100644 --- a/utils/rbutilqt/base/autodetection.cpp +++ b/utils/rbutilqt/base/autodetection.cpp @@ -259,7 +259,7 @@ void Autodetection::mergePatcher(void) n = ipod_scan(&ipod); // FIXME: handle more than one Ipod connected in ipodpatcher. if(n == 1) { - LOG_INFO() << "Ipod found:" << ipod.modelstr << "at" << ipod.diskname; + LOG_INFO() << "iPod found:" << ipod.modelstr << "at" << ipod.diskname; // since resolveMountPoint is doing exact matches we need to select // the correct partition. QString mp(ipod.diskname); @@ -280,7 +280,7 @@ void Autodetection::mergePatcher(void) updateDetectedDevice(d); } else { - LOG_INFO() << "ipodpatcher: no Ipod found." << n; + LOG_INFO() << "ipodpatcher: no iPod found." << n; } ipod_dealloc_buffer(&ipod); diff --git a/utils/rbutilqt/base/bootloaderinstallipod.cpp b/utils/rbutilqt/base/bootloaderinstallipod.cpp index 1e5b1c39b2..fae93da994 100644 --- a/utils/rbutilqt/base/bootloaderinstallipod.cpp +++ b/utils/rbutilqt/base/bootloaderinstallipod.cpp @@ -83,7 +83,7 @@ void BootloaderInstallIpod::installStage2(void) QCoreApplication::processEvents(); if(ipod_reopen_rw(&ipod) < 0) { - emit logItem(tr("Could not open Ipod in R/W mode"), LOGERROR); + emit logItem(tr("Could not open iPod in R/W mode"), LOGERROR); emit done(true); return; } @@ -152,7 +152,7 @@ bool BootloaderInstallIpod::uninstall(void) } if (ipod_reopen_rw(&ipod) < 0) { - emit logItem(tr("Could not open Ipod in R/W mode"), LOGERROR); + emit logItem(tr("Could not open iPod in R/W mode"), LOGERROR); emit done(true); return false; } @@ -259,16 +259,16 @@ bool BootloaderInstallIpod::ipodInitialize(struct ipod_t *ipod) } int result = ipod_open(ipod, 1); if(result == -2) { - emit logItem(tr("Could not open Ipod: permission denied"), LOGERROR); + emit logItem(tr("Could not open iPod: permission denied"), LOGERROR); return false; } else if(result < 0) { - emit logItem(tr("Could not open Ipod"), LOGERROR); + emit logItem(tr("Could not open iPod"), LOGERROR); return false; } if(read_partinfo(ipod, 1) < 0) { - emit logItem(tr("Error reading partition table - possibly not an Ipod"), LOGERROR); + emit logItem(tr("Error reading partition table - possibly not an iPod"), LOGERROR); ipod_close(ipod); return false; } diff --git a/utils/rbutilqt/base/bootloaderinstallsansa.cpp b/utils/rbutilqt/base/bootloaderinstallsansa.cpp index d2c17113e8..dbbab0f5ae 100644 --- a/utils/rbutilqt/base/bootloaderinstallsansa.cpp +++ b/utils/rbutilqt/base/bootloaderinstallsansa.cpp @@ -218,7 +218,7 @@ BootloaderInstallBase::BootloaderType BootloaderInstallSansa::installed(void) bool BootloaderInstallSansa::sansaInitialize(struct sansa_t *sansa) { - // if the ipod was already opened make sure to close it first. + // if the player was already opened make sure to close it first. #if defined(Q_OS_WIN32) if(sansa->dh != INVALID_HANDLE_VALUE) #else diff --git a/utils/rbutilqt/changelog.txt b/utils/rbutilqt/changelog.txt index d656142e69..d0ebc4ea21 100644 --- a/utils/rbutilqt/changelog.txt +++ b/utils/rbutilqt/changelog.txt @@ -43,7 +43,7 @@ Version 1.5.0 * Server Downloads changed to https. Version 1.5.1 -* Fix bootloader installation for Ipods. +* Fix bootloader installation for iPods. * Fix bootloader installation for Sandisk players. * Improve responsiveness on install / uninstall. * Enable Themes installation if themes are selected. diff --git a/utils/rbutilqt/configure.cpp b/utils/rbutilqt/configure.cpp index d8a2a5a4c2..a3c2484c7b 100644 --- a/utils/rbutilqt/configure.cpp +++ b/utils/rbutilqt/configure.cpp @@ -797,7 +797,7 @@ void Config::autodetect() if(PlayerBuildInfo::instance()->value( PlayerBuildInfo::BootloaderMethod, detected.at(0).device) == "ipod") { msg = tr("%1 \"MacPod\" found!\n" - "Rockbox needs a FAT formatted Ipod (so-called \"WinPod\") " + "Rockbox needs a FAT formatted iPod (so-called \"WinPod\") " "to run. ").arg(PlayerBuildInfo::instance()->value( PlayerBuildInfo::DisplayName, detected.at(0).device).toString()); diff --git a/utils/rbutilqt/lang/rbutil_cs.ts b/utils/rbutilqt/lang/rbutil_cs.ts index bc75a3da39..2b95422a4f 100644 --- a/utils/rbutilqt/lang/rbutil_cs.ts +++ b/utils/rbutilqt/lang/rbutil_cs.ts @@ -669,8 +669,8 @@ Viz https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode - Nelze otevřít Ipod pro čtení a zápis + Could not open iPod in R/W mode + Nelze otevřít iPod pro čtení a zápis @@ -724,18 +724,18 @@ Viz https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied - Nelze otevřít Ipod: nepovolený přístup + Could not open iPod: permission denied + Nelze otevřít iPod: nepovolený přístup - Could not open Ipod - Nelze otevřít Ipod + Could not open iPod + Nelze otevřít iPod - Error reading partition table - possibly not an Ipod - Chyba při čtení tabulky oddílů - možná to není Ipod + Error reading partition table - possibly not an iPod + Chyba při čtení tabulky oddílů - možná to není iPod @@ -1459,9 +1459,9 @@ Musíte přepnout váš přehrávač do MSC módu, aby instalace proběhla. %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. Nalezen %1 "MacPod"! -Rockbox potřebuje k běhu Ipod naformátovaný jako FAT (tzv. "WinPod"). +Rockbox potřebuje k běhu iPod naformátovaný jako FAT (tzv. "WinPod"). Fatal error diff --git a/utils/rbutilqt/lang/rbutil_de.ts b/utils/rbutilqt/lang/rbutil_de.ts index 9c17153a8d..a85e1af7a3 100644 --- a/utils/rbutilqt/lang/rbutil_de.ts +++ b/utils/rbutilqt/lang/rbutil_de.ts @@ -664,8 +664,8 @@ Siehe https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode - Kann Ipod nicht im R/W-Modus öffnen + Could not open iPod in R/W mode + Kann iPod nicht im R/W-Modus öffnen @@ -704,13 +704,13 @@ Siehe https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied - Konnte Ipod nicht öffnen: Zugriff verweigert + Could not open iPod: permission denied + Konnte iPod nicht öffnen: Zugriff verweigert - Could not open Ipod - Konnte Ipod nicht öffnen + Could not open iPod + Konnte iPod nicht öffnen @@ -739,8 +739,8 @@ Siehe https://www.rockbox.org/wiki/IpodConversionToFAT32 - Error reading partition table - possibly not an Ipod - Fehler beim Lesen der Partitionstabelle - möglicherweise kein Ipod + Error reading partition table - possibly not an iPod + Fehler beim Lesen der Partitionstabelle - möglicherweise kein iPod @@ -1425,9 +1425,9 @@ Rockbox Utility kann mit diesen Proxy-Einstellungen nicht arbeiten. Bitte sicher %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. %1 "MacPod" gefunden! -Rockbox benötigt einen mit dem Dateisystem FAT formatierten Ipod (sogenannter "WinPod"). +Rockbox benötigt einen mit dem Dateisystem FAT formatierten iPod (sogenannter "WinPod"). diff --git a/utils/rbutilqt/lang/rbutil_fi.ts b/utils/rbutilqt/lang/rbutil_fi.ts index ea670b126a..33ee8af75e 100644 --- a/utils/rbutilqt/lang/rbutil_fi.ts +++ b/utils/rbutilqt/lang/rbutil_fi.ts @@ -655,7 +655,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode + Could not open iPod in R/W mode @@ -705,12 +705,12 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied + Could not open iPod: permission denied - Could not open Ipod + Could not open iPod @@ -730,7 +730,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Error reading partition table - possibly not an Ipod + Error reading partition table - possibly not an iPod @@ -1289,7 +1289,7 @@ Rockbox Utility can't work with this proxy settings. Make sure the system p %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. diff --git a/utils/rbutilqt/lang/rbutil_fr.ts b/utils/rbutilqt/lang/rbutil_fr.ts index da7c181ef0..e5c3692f83 100644 --- a/utils/rbutilqt/lang/rbutil_fr.ts +++ b/utils/rbutilqt/lang/rbutil_fr.ts @@ -669,8 +669,8 @@ Veuillez vous référer à https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode - Impossible d'accéder à l'Ipod en lecture et écriture + Could not open iPod in R/W mode + Impossible d'accéder à l'iPod en lecture et écriture @@ -719,13 +719,13 @@ Veuillez vous référer à https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied - Impossible d'accéder à l'Ipod: accès refusé + Could not open iPod: permission denied + Impossible d'accéder à l'iPod: accès refusé - Could not open Ipod - Impossible d'accéder à l'Ipod + Could not open iPod + Impossible d'accéder à l'iPod @@ -744,8 +744,8 @@ Veuillez vous référer à https://www.rockbox.org/wiki/IpodConversionToFAT32 - Error reading partition table - possibly not an Ipod - Erreur lors de la lecteur de la table des partitions; ce lecteur audio n'est peut-être pas un Ipod + Error reading partition table - possibly not an iPod + Erreur lors de la lecteur de la table des partitions; ce lecteur audio n'est peut-être pas un iPod @@ -1256,9 +1256,9 @@ L'Utilitaire Rockbox ne peut pas fonctionner avec ces réglages proxy. Vér %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. %1 "MacPod" détecté ! -Rockbox ne peut fonctionner que sur un Ipod utilisant le système de fichier FAT (aussi appelé un "WinPod"). +Rockbox ne peut fonctionner que sur un iPod utilisant le système de fichier FAT (aussi appelé un "WinPod"). diff --git a/utils/rbutilqt/lang/rbutil_gr.ts b/utils/rbutilqt/lang/rbutil_gr.ts index a4d1e9ca70..185222f869 100644 --- a/utils/rbutilqt/lang/rbutil_gr.ts +++ b/utils/rbutilqt/lang/rbutil_gr.ts @@ -659,8 +659,8 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode - Αδύνατη η προσπέλαση του ipod με τρόπο RW + Could not open iPod in R/W mode + Αδύνατη η προσπέλαση του iPod με τρόπο RW @@ -709,13 +709,13 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied + Could not open iPod: permission denied - Could not open Ipod - Αδύνατο το άνοιγμα του ipod + Could not open iPod + Αδύνατο το άνοιγμα του iPod @@ -734,7 +734,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Error reading partition table - possibly not an Ipod + Error reading partition table - possibly not an iPod @@ -1310,7 +1310,7 @@ Rockbox Utility can't work with this proxy settings. Make sure the system p %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. diff --git a/utils/rbutilqt/lang/rbutil_he.ts b/utils/rbutilqt/lang/rbutil_he.ts index 93f1c6861e..a71bbbf04f 100644 --- a/utils/rbutilqt/lang/rbutil_he.ts +++ b/utils/rbutilqt/lang/rbutil_he.ts @@ -660,7 +660,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode + Could not open iPod in R/W mode לא מצליח לפתוח את האייפוד במצב קריאה/כתיבה @@ -710,12 +710,12 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied + Could not open iPod: permission denied לא מצליח לפתוח את האייפוד: הגישה נדחתה - Could not open Ipod + Could not open iPod לא מצליח לפתוח אייפוד @@ -735,7 +735,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Error reading partition table - possibly not an Ipod + Error reading partition table - possibly not an iPod שגיאה בקריאת טבלת המחיצות - ככל הנראה נגן זה אינו אייפוד @@ -1313,7 +1313,7 @@ Rockbox Utility can't work with this proxy settings. Make sure the system p %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. נמצא %1 "MacPod"! רוקבוקס זקוקה לאייפוד המפורמט בשיטת FAT (נגנים אלי מכונים WinPod) על מנת לרוץ. diff --git a/utils/rbutilqt/lang/rbutil_it.ts b/utils/rbutilqt/lang/rbutil_it.ts index 9039dd870e..6766057ff6 100644 --- a/utils/rbutilqt/lang/rbutil_it.ts +++ b/utils/rbutilqt/lang/rbutil_it.ts @@ -665,8 +665,8 @@ Consulta https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode - Impossibile accedere all'Ipod in R/W + Could not open iPod in R/W mode + Impossibile accedere all'iPod in R/W @@ -720,18 +720,18 @@ Consulta https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied + Could not open iPod: permission denied Impossibile aprire l'iPod: accesso negato - Could not open Ipod - impossibile accedere all'Ipod + Could not open iPod + impossibile accedere all'iPod - Error reading partition table - possibly not an Ipod - Errore nel leggere la tabelle delle partizioni - è possibile che non sia un Ipod + Error reading partition table - possibly not an iPod + Errore nel leggere la tabelle delle partizioni - è possibile che non sia un iPod @@ -1369,7 +1369,7 @@ Rockbox Utility non può funzionare con queste impostazioni. Assicurati che il p %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. Rilevato %1 «MacPod»! Per funzionare, Rockbox richiede un iPod con file system FAT (il cosiddetto «WinPod»). diff --git a/utils/rbutilqt/lang/rbutil_ja.ts b/utils/rbutilqt/lang/rbutil_ja.ts index 247edde3a3..6e4826a7c2 100644 --- a/utils/rbutilqt/lang/rbutil_ja.ts +++ b/utils/rbutilqt/lang/rbutil_ja.ts @@ -660,7 +660,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode + Could not open iPod in R/W mode iPod に読み書きできるようにアクセスすることができませんでした @@ -700,12 +700,12 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied + Could not open iPod: permission denied iPod にアクセスすることができませんでした: アクセス権限がありません - Could not open Ipod + Could not open iPod iPod にアクセスできませんでした @@ -735,7 +735,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Error reading partition table - possibly not an Ipod + Error reading partition table - possibly not an iPod パーティションテーブルの読み込みのエラー - iPod ではない可能性があります @@ -1412,7 +1412,7 @@ Rockbox Utility は、このプロキシの設定では動作できません。 %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. %1 Macintosh 専用の iPod が見つかりました。 Rockbox を実行するには、FAT 形式でフォーマットされた iPod ("WinMad")が必要です。 diff --git a/utils/rbutilqt/lang/rbutil_ko.ts b/utils/rbutilqt/lang/rbutil_ko.ts index c1223623fd..8347c9b81e 100644 --- a/utils/rbutilqt/lang/rbutil_ko.ts +++ b/utils/rbutilqt/lang/rbutil_ko.ts @@ -663,7 +663,7 @@ https://www.rockbox.org/wiki/IpodConversionToFAT32 참고 - Could not open Ipod in R/W mode + Could not open iPod in R/W mode R/W 모드에서 iPod을 열 수 없음 @@ -703,13 +703,13 @@ https://www.rockbox.org/wiki/IpodConversionToFAT32 참고 - Could not open Ipod: permission denied + Could not open iPod: permission denied iPod을 열 수 없음: 권한이 거부됨 - Could not open Ipod - Ipod을 열 수 없음 + Could not open iPod + iPod을 열 수 없음 @@ -744,8 +744,8 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Error reading partition table - possibly not an Ipod - 파티션 테이블을 읽는 중 오류 발생 - 아마도 Ipod이 아닐 수도 있음 + Error reading partition table - possibly not an iPod + 파티션 테이블을 읽는 중 오류 발생 - 아마도 iPod이 아닐 수도 있음 @@ -1260,7 +1260,7 @@ Select your device and Mountpoint manually. %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. @@ -1441,7 +1441,7 @@ Rockbox Utility can't work with this proxy settings. Make sure the system p %1 "MacPod"를 찾았습니다! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. %1 "MacPod" gefunden! 록박스를 실행하려면 FAT 형식의 아이팟(소위 "WinPod")이 필요합니다. diff --git a/utils/rbutilqt/lang/rbutil_nl.ts b/utils/rbutilqt/lang/rbutil_nl.ts index e5312ce2a2..e9e8c01887 100644 --- a/utils/rbutilqt/lang/rbutil_nl.ts +++ b/utils/rbutilqt/lang/rbutil_nl.ts @@ -668,8 +668,8 @@ Zie https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode - Kon Ipod niet openen in R/W modus + Could not open iPod in R/W mode + Kon iPod niet openen in R/W modus @@ -718,18 +718,18 @@ Zie https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied - Kon Ipod niet openen: toestemming geweigerd + Could not open iPod: permission denied + Kon iPod niet openen: toestemming geweigerd - Could not open Ipod - Kon Ipod niet openen + Could not open iPod + Kon iPod niet openen - Error reading partition table - possibly not an Ipod - Fout tijdens lezen van de partitie tabel - mogelijks geen Ipod + Error reading partition table - possibly not an iPod + Fout tijdens lezen van de partitie tabel - mogelijks geen iPod @@ -1254,9 +1254,9 @@ Rockbox Utility kan niet werken met deze instellingen. Stel de systeemproxy-inst %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. %1 "MacPod" gevonden! -Rockbox heeft een FAT-geformattered Ipod (een zogenaamde "WinPod") nodig om te kunnen draaien. +Rockbox heeft een FAT-geformattered iPod (een zogenaamde "WinPod") nodig om te kunnen draaien. diff --git a/utils/rbutilqt/lang/rbutil_pl.ts b/utils/rbutilqt/lang/rbutil_pl.ts index be09ef5614..ec9805e585 100644 --- a/utils/rbutilqt/lang/rbutil_pl.ts +++ b/utils/rbutilqt/lang/rbutil_pl.ts @@ -673,7 +673,7 @@ Zobacz https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode + Could not open iPod in R/W mode Nie można otworzyć iPoda w trybie odczytu/zapisu @@ -728,17 +728,17 @@ Zobacz https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied + Could not open iPod: permission denied Nie można otworzyć iPoda: dostęp zabroniony - Could not open Ipod + Could not open iPod Nie można otworzyć iPoda - Error reading partition table - possibly not an Ipod + Error reading partition table - possibly not an iPod Błąd odczytu tablicy partycji - możliwe że urządzenie nie jest iPodem @@ -1366,9 +1366,9 @@ Rockbox Utility nie może pracować z tymi ustawieniami. Proszę sprawdź, że u %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. znaleziono \"MaxPod\" %1! -Rockbox obsługuje wyłącznie Ipody z systemem plików FAT (tak zwane \"WinPod\"). +Rockbox obsługuje wyłącznie iPody z systemem plików FAT (tak zwane \"WinPod\"). Fatal error diff --git a/utils/rbutilqt/lang/rbutil_pt.ts b/utils/rbutilqt/lang/rbutil_pt.ts index 44d652d6ac..db51435926 100644 --- a/utils/rbutilqt/lang/rbutil_pt.ts +++ b/utils/rbutilqt/lang/rbutil_pt.ts @@ -659,8 +659,8 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode - Falha na abertura do Ipod em modo de leitura e escrita (R/W) + Could not open iPod in R/W mode + Falha na abertura do iPod em modo de leitura e escrita (R/W) @@ -709,13 +709,13 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied + Could not open iPod: permission denied - Could not open Ipod - Não consegiu abrir o Ipod + Could not open iPod + Não consegiu abrir o iPod @@ -734,7 +734,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Error reading partition table - possibly not an Ipod + Error reading partition table - possibly not an iPod @@ -1311,7 +1311,7 @@ Rockbox Utility can't work with this proxy settings. Make sure the system p %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. diff --git a/utils/rbutilqt/lang/rbutil_pt_BR.ts b/utils/rbutilqt/lang/rbutil_pt_BR.ts index 11798b877e..d4c5c82222 100644 --- a/utils/rbutilqt/lang/rbutil_pt_BR.ts +++ b/utils/rbutilqt/lang/rbutil_pt_BR.ts @@ -658,8 +658,8 @@ - Could not open Ipod in R/W mode - Não foi possível abrir o Ipod em modo R/W (Leitura/Escrita) + Could not open iPod in R/W mode + Não foi possível abrir o iPod em modo R/W (Leitura/Escrita) @@ -693,13 +693,13 @@ - Could not open Ipod - Não foi possível abrir o Ipod + Could not open iPod + Não foi possível abrir o iPod - Error reading partition table - possibly not an Ipod - Erro lendo tabela de partição - possivelmente não é um Ipod + Error reading partition table - possibly not an iPod + Erro lendo tabela de partição - possivelmente não é um iPod @@ -730,8 +730,8 @@ Veja https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied - Não foi possível abrir o Ipod: permissão negada + Could not open iPod: permission denied + Não foi possível abrir o iPod: permissão negada @@ -1399,9 +1399,9 @@ Por favor, configure o motor TTS. %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. %1 encontrado "MacPod"! -O Rockbox precisa de um Ipod formatado como FAT (chamado de "WinPod") para funcionar. +O Rockbox precisa de um iPod formatado como FAT (chamado de "WinPod") para funcionar. diff --git a/utils/rbutilqt/lang/rbutil_ru.ts b/utils/rbutilqt/lang/rbutil_ru.ts index aec30a697f..53b79a7561 100644 --- a/utils/rbutilqt/lang/rbutil_ru.ts +++ b/utils/rbutilqt/lang/rbutil_ru.ts @@ -661,7 +661,7 @@ - Could not open Ipod in R/W mode + Could not open iPod in R/W mode Не удалось открыть iPod в режиме записи @@ -686,7 +686,7 @@ - Could not open Ipod + Could not open iPod Не удалось открыть iPod @@ -706,7 +706,7 @@ - Error reading partition table - possibly not an Ipod + Error reading partition table - possibly not an iPod Сбой чтения таблицы разделов - возможно, это не iPod @@ -738,7 +738,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied + Could not open iPod: permission denied Не удалось открыть iPod: доступ запрещён @@ -1402,7 +1402,7 @@ Please configure TTS engine. %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. %1 является MacPod'ом! Для работы, Rockbox нужен iPod форматированный в FAT (так называемый "WinPod"). diff --git a/utils/rbutilqt/lang/rbutil_tr.ts b/utils/rbutilqt/lang/rbutil_tr.ts index 677e82b6e9..f9181ff8c9 100644 --- a/utils/rbutilqt/lang/rbutil_tr.ts +++ b/utils/rbutilqt/lang/rbutil_tr.ts @@ -660,7 +660,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode + Could not open iPod in R/W mode @@ -715,17 +715,17 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied + Could not open iPod: permission denied - Could not open Ipod + Could not open iPod - Error reading partition table - possibly not an Ipod + Error reading partition table - possibly not an iPod @@ -1293,19 +1293,19 @@ See https://www.rockbox.org/wiki/SansaE200Install Orijinal bellenim (firmware) mevcut değil: %1 - Searching for ipods + Searching for iPods iPod'lar aranıyor - No Ipods found + No iPods found Herhangi bir iPod bulunamadı - Too many Ipods found + Too many iPods found Çok sayıda iPod bulundu - could not open ipod + could not open iPod iPod açılamadı @@ -1341,7 +1341,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 Ayrıntılı bilgi için: https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in RW mode + Could not open iPod in RW mode iPod RW modunda açılamadı @@ -1711,7 +1711,7 @@ You need to change your player to MSC mode for installation. %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. diff --git a/utils/rbutilqt/lang/rbutil_zh_CN.ts b/utils/rbutilqt/lang/rbutil_zh_CN.ts index 38746fa6c8..4de2f875be 100644 --- a/utils/rbutilqt/lang/rbutil_zh_CN.ts +++ b/utils/rbutilqt/lang/rbutil_zh_CN.ts @@ -663,7 +663,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode + Could not open iPod in R/W mode 无法打开iPod读写模式 @@ -713,12 +713,12 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied + Could not open iPod: permission denied 无法打开iPod:拒绝访问 - Could not open Ipod + Could not open iPod 无法打开iPod @@ -745,7 +745,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Error reading partition table - possibly not an Ipod + Error reading partition table - possibly not an iPod 读取分区表时出错 - 可能不是 iPod @@ -1268,7 +1268,7 @@ Rockbox Utility 无法使用此代理设置。请确保系统代理设置正确 %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. %1 发现Mac格式的iPod!! Rockbox需要FAT格式的iPod才能运行。 diff --git a/utils/rbutilqt/lang/rbutil_zh_TW.ts b/utils/rbutilqt/lang/rbutil_zh_TW.ts index b1b7265a6f..ee9a1691ec 100644 --- a/utils/rbutilqt/lang/rbutil_zh_TW.ts +++ b/utils/rbutilqt/lang/rbutil_zh_TW.ts @@ -655,7 +655,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod in R/W mode + Could not open iPod in R/W mode @@ -705,12 +705,12 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Could not open Ipod: permission denied + Could not open iPod: permission denied - Could not open Ipod + Could not open iPod @@ -730,7 +730,7 @@ See https://www.rockbox.org/wiki/IpodConversionToFAT32 - Error reading partition table - possibly not an Ipod + Error reading partition table - possibly not an iPod @@ -1223,7 +1223,7 @@ Rockbox Utility can't work with this proxy settings. Make sure the system p %1 "MacPod" found! -Rockbox needs a FAT formatted Ipod (so-called "WinPod") to run. +Rockbox needs a FAT formatted iPod (so-called "WinPod") to run. diff --git a/utils/rbutilqt/rbutil.ini b/utils/rbutilqt/rbutil.ini index f4711afd99..2611b047d5 100644 --- a/utils/rbutilqt/rbutil.ini +++ b/utils/rbutilqt/rbutil.ini @@ -241,7 +241,7 @@ playerpic=iriverh10 encoder=rbspeex [ipod1g2g] -name="Ipod (1st / 2nd gen)" +name="iPod (1st / 2nd gen)" bootloadermethod=ipod bootloadername=/ipod/bootloader-ipod1g2g.ipod manualname= @@ -251,7 +251,7 @@ playerpic=ipod1g2g encoder=rbspeex [ipodcolor] -name="Ipod Color / Photo / U2 (4th gen)" +name="iPod Color / Photo / U2 (4th gen)" bootloadermethod=ipod bootloadername=/ipod/bootloader-ipodcolor.ipod manualname= @@ -261,7 +261,7 @@ playerpic=ipodcolor encoder=rbspeex [ipodnano1g] -name="Ipod Nano (1st gen)" +name="iPod Nano (1st gen)" bootloadermethod=ipod bootloadername=/ipod/bootloader-ipodnano1g.ipod manualname= @@ -271,7 +271,7 @@ playerpic=ipodnano1g encoder=rbspeex [ipodnano2g] -name="Ipod Nano (2nd gen)" +name="iPod Nano (2nd gen)" bootloadermethod=ipod bootloadername=/ipod/bootloader-ipodnano2g.ipodx manualname= @@ -281,7 +281,7 @@ playerpic=ipodnano2g encoder=rbspeex [ipod4g] -name="Ipod (4th gen, greyscale)" +name="iPod (4th gen, greyscale)" bootloadermethod=ipod bootloadername=/ipod/bootloader-ipod4g.ipod manualname= @@ -291,7 +291,7 @@ playerpic=ipod4g encoder=rbspeex [ipodvideo] -name="Ipod Video (5th gen)" +name="iPod Video (5th gen)" bootloadermethod=ipod bootloadername=/ipod/bootloader-ipodvideo.ipod manualname= @@ -301,7 +301,7 @@ playerpic=ipodvideo encoder=rbspeex [ipod6g] -name="Ipod Classic (6th gen)" +name="iPod Classic (6th gen)" bootloadermethod=s5l bootloadername=/ipod/bootloader-ipod6g.ipod manualname= @@ -311,7 +311,7 @@ playerpic=ipod6g encoder=rbspeex [ipod3g] -name="Ipod (3rd gen)" +name="iPod (3rd gen)" bootloadermethod=ipod bootloadername=/ipod/bootloader-ipod3g.ipod manualname= @@ -321,7 +321,7 @@ playerpic=ipod3g encoder=rbspeex [ipodmini1g] -name="Ipod Mini (1st gen)" +name="iPod Mini (1st gen)" bootloadermethod=ipod bootloadername=/ipod/bootloader-ipodmini1g.ipod brand=Apple @@ -330,7 +330,7 @@ playerpic=ipodmini1g encoder=rbspeex [ipodmini2g] -name="Ipod Mini (2nd gen)" +name="iPod Mini (2nd gen)" bootloadermethod=ipod bootloadername=/ipod/bootloader-ipodmini2g.ipod manualname=ipodmini1g @@ -1015,25 +1015,25 @@ pathsuffix=/FunKey ; each section uses a USB VID / PID string as section name. ; name: human readable string to show the user when this device is detected. [05ac1240] -name="Apple Ipod Nano (Second Generation, DFU Mode)" +name="Apple iPod Nano (Second Generation, DFU Mode)" [05ac1262] -name="Apple Ipod Nano (Third Generation)" +name="Apple iPod Nano (Third Generation)" [05ac1263] -name="Apple Ipod Nano (Fourth Generation)" +name="Apple iPod Nano (Fourth Generation)" [05ac1255] -name="Apple Ipod Nano (Fourth Generation, DFU Mode)" +name="Apple iPod Nano (Fourth Generation, DFU Mode)" [05ac1265] -name="Apple Ipod Nano (Fifth Generation)" +name="Apple iPod Nano (Fifth Generation)" [05ac1266] -name="Apple Ipod Nano (Sixth Generation)" +name="Apple iPod Nano (Sixth Generation)" [05ac1267] -name="Apple Ipod Nano (Seventh Generation)" +name="Apple iPod Nano (Seventh Generation)" [041e4133] name="Creative Zen series (Removable Disk Mode)" From 626643b46df2db097fe5cef9295833e2f1783240 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Tue, 29 Sep 2026 23:00:06 +0300 Subject: [PATCH 314/408] ipodpatcher: Fix case of "iPod" in logs Functions, structs and variables not modified. Change-Id: I4c0ea679e3e2225d3d4d0bcbe5db5071e5a18b47 --- utils/ipodpatcher/ipodpatcher.c | 16 +++++++-------- utils/ipodpatcher/main.c | 36 ++++++++++++++++----------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/utils/ipodpatcher/ipodpatcher.c b/utils/ipodpatcher/ipodpatcher.c index de9ca5b3f7..941055294b 100644 --- a/utils/ipodpatcher/ipodpatcher.c +++ b/utils/ipodpatcher/ipodpatcher.c @@ -257,7 +257,7 @@ int read_partinfo(struct ipod_t* ipod, int silent) if ((ipod->pinfo[0].type != 0) || (ipod->pinfo[0].size == 0) || ((ipod->pinfo[1].type != 0xb) && (ipod->pinfo[1].type != 0xc) && (ipod->pinfo[1].type != PARTTYPE_HFS))) { - if (!silent) fprintf(stderr,"[ERR] Partition layout is not an ipod\n"); + if (!silent) fprintf(stderr,"[ERR] Partition layout is not an iPod\n"); return -1; } @@ -650,7 +650,7 @@ int add_new_image(struct ipod_t* ipod, char* imagename, char* filename, int type } if (memcmp(header+4, ipod->modelname,4)!=0) { - fprintf(stderr,"[ERR] Model name in input file (%c%c%c%c) doesn't match ipod model (%s)\n", + fprintf(stderr,"[ERR] Model name in input file (%c%c%c%c) doesn't match iPod model (%s)\n", header[4],header[5],header[6],header[7], ipod->modelname); close(infile); return -1; @@ -936,7 +936,7 @@ int add_bootloader(struct ipod_t* ipod, char* filename, int type) } if (memcmp(header+4, ipod->modelname,4)!=0) { - fprintf(stderr,"[ERR] Model name in input file (%c%c%c%c) doesn't match ipod model (%s)\n", + fprintf(stderr,"[ERR] Model name in input file (%c%c%c%c) doesn't match iPod model (%s)\n", header[4],header[5],header[6],header[7], ipod->modelname); close(infile); return -1; @@ -1233,7 +1233,7 @@ int write_firmware(struct ipod_t* ipod, char* filename, int type) } if (memcmp(header+4, ipod->modelname,4)!=0) { - fprintf(stderr,"[ERR] Model name in input file (%c%c%c%c) doesn't match ipod model (%s)\n", + fprintf(stderr,"[ERR] Model name in input file (%c%c%c%c) doesn't match iPod model (%s)\n", header[4],header[5],header[6],header[7], ipod->modelname); close(infile); return -1; @@ -1810,10 +1810,10 @@ int ipod_scan(struct ipod_t* ipod) } #ifdef __WIN32__ - printf("[INFO] Ipod found - %s (\"%s\") - disk device %d\n", + printf("[INFO] iPod found - %s (\"%s\") - disk device %d\n", ipod->modelstr,ipod->macpod ? "macpod" : "winpod",i); #else - printf("[INFO] Ipod found - %s (\"%s\") - %s\n", + printf("[INFO] iPod found - %s (\"%s\") - %s\n", ipod->modelstr,ipod->macpod ? "macpod" : "winpod",ipod->diskname); #endif n++; @@ -1828,7 +1828,7 @@ int ipod_scan(struct ipod_t* ipod) memcpy(ipod, &ipod_found, sizeof(struct ipod_t)); } else if(n == 0 && denied) { - printf("[ERR] FATAL: Permission denied on %d device(s) and no ipod detected.\n", denied); + printf("[ERR] FATAL: Permission denied on %d device(s) and no iPod detected.\n", denied); #ifdef __WIN32__ printf("[ERR] You need to run this program with administrator priviledges!\n"); #else @@ -1855,7 +1855,7 @@ int write_dos_partition_table(struct ipod_t* ipod) /* Only support 512-byte sectors at the moment */ if ( ipod->sector_size != 512 ) { - fprintf(stderr,"[ERR] Only ipods with 512 bytes per sector are supported.\n"); + fprintf(stderr,"[ERR] Only iPods with 512 bytes per sector are supported.\n"); return -1; } if(ipod->sectorbuf == NULL) { diff --git a/utils/ipodpatcher/main.c b/utils/ipodpatcher/main.c index e207bf782f..f199173fa9 100644 --- a/utils/ipodpatcher/main.c +++ b/utils/ipodpatcher/main.c @@ -61,8 +61,8 @@ void print_macpod_warning(void) { printf("[INFO] ************************************************************************\n"); printf("[INFO] *** WARNING FOR ROCKBOX USERS\n"); - printf("[INFO] *** You must convert this ipod to FAT32 format (aka a \"winpod\")\n"); - printf("[INFO] *** if you want to run Rockbox. Rockbox WILL NOT work on this ipod.\n"); + printf("[INFO] *** You must convert this iPod to FAT32 format (aka a \"winpod\")\n"); + printf("[INFO] *** if you want to run Rockbox. Rockbox WILL NOT work on this iPod.\n"); printf("[INFO] *** See http://www.rockbox.org/wiki/IpodConversionToFAT32\n"); printf("[INFO] ************************************************************************\n"); } @@ -103,21 +103,21 @@ void print_usage(void) fprintf(stderr,"The .ipodx extension is used for encrypted images for the 2nd Gen Nano.\n\n"); #ifdef __WIN32__ - fprintf(stderr,"DISKNO is the number (e.g. 2) Windows has assigned to your ipod's hard disk.\n"); + fprintf(stderr,"DISKNO is the number (e.g. 2) Windows has assigned to your iPod's hard disk.\n"); fprintf(stderr,"The first hard disk in your computer (i.e. C:\\) will be disk 0, the next disk\n"); fprintf(stderr,"will be disk 1 etc. ipodpatcher will refuse to access a disk unless it\n"); - fprintf(stderr,"can identify it as being an ipod.\n"); + fprintf(stderr,"can identify it as being an iPod.\n"); fprintf(stderr,"\n"); #else #if defined(linux) || defined (__linux) - fprintf(stderr,"\"device\" is the device node (e.g. /dev/sda) assigned to your ipod.\n"); + fprintf(stderr,"\"device\" is the device node (e.g. /dev/sda) assigned to your iPod.\n"); #elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) - fprintf(stderr,"\"device\" is the device node (e.g. /dev/da1) assigned to your ipod.\n"); + fprintf(stderr,"\"device\" is the device node (e.g. /dev/da1) assigned to your iPod.\n"); #elif defined(__APPLE__) && defined(__MACH__) - fprintf(stderr,"\"device\" is the device node (e.g. /dev/disk1) assigned to your ipod.\n"); + fprintf(stderr,"\"device\" is the device node (e.g. /dev/disk1) assigned to your iPod.\n"); #endif fprintf(stderr,"ipodpatcher will refuse to access a disk unless it can identify it as being\n"); - fprintf(stderr,"an ipod.\n"); + fprintf(stderr,"an iPod.\n"); #endif } @@ -169,7 +169,7 @@ int main(int argc, char* argv[]) if ((argc > 1) && (strcmp(argv[1],"--scan")==0)) { if (ipod_scan(&ipod) == 0) - fprintf(stderr,"[ERR] No ipods found.\n"); + fprintf(stderr,"[ERR] No iPods found.\n"); return IPOD_NOT_FOUND; } @@ -186,10 +186,10 @@ int main(int argc, char* argv[]) /* Autoscan for ipods */ n = ipod_scan(&ipod); if (n==0) { - fprintf(stderr,"[ERR] No ipods found, aborting\n"); - fprintf(stderr,"[ERR] Please connect your ipod and ensure it is in disk mode\n"); + fprintf(stderr,"[ERR] No iPods found, aborting\n"); + fprintf(stderr,"[ERR] Please connect your iPod and ensure it is in disk mode\n"); #if defined(__APPLE__) && defined(__MACH__) - fprintf(stderr,"[ERR] Also ensure that itunes is closed, and that your ipod is not mounted.\n"); + fprintf(stderr,"[ERR] Also ensure that itunes is closed, and that your iPod is not mounted.\n"); #elif !defined(__WIN32__) if (geteuid()!=0) { fprintf(stderr,"[ERR] You may also need to run ipodpatcher as root.\n"); @@ -197,8 +197,8 @@ int main(int argc, char* argv[]) #endif fprintf(stderr,"[ERR] Please refer to the Rockbox manual if you continue to have problems.\n"); } else if (n > 1) { - fprintf(stderr,"[ERR] %d ipods found, aborting\n",n); - fprintf(stderr,"[ERR] Please connect only one ipod and re-run ipodpatcher.\n"); + fprintf(stderr,"[ERR] %d iPods found, aborting\n",n); + fprintf(stderr,"[ERR] Please connect only one iPod and re-run ipodpatcher.\n"); return IPOD_MULTIPLE_DEVICES; } else if (n == 1 && ipod.macpod) { return IPOD_WRONG_TYPE; @@ -396,7 +396,7 @@ int main(int argc, char* argv[]) ipod_get_ramsize(&ipod); } - printf("[INFO] Ipod model: %s ",ipod.modelstr); + printf("[INFO] iPod model: %s ",ipod.modelstr); if (ipod.ramsize > 0) { printf("(%dMB RAM) ",ipod.ramsize); } printf("(\"%s\")\n",ipod.macpod ? "macpod" : "winpod"); @@ -564,7 +564,7 @@ int main(int argc, char* argv[]) } else if (action==FORMAT_PARTITION) { printf("WARNING!!! YOU ARE ABOUT TO USE AN EXPERIMENTAL FEATURE.\n"); printf("ALL DATA ON YOUR IPOD WILL BE ERASED.\n"); - printf("Are you sure you want to format your ipod? (y/n):"); + printf("Are you sure you want to format your iPod? (y/n):"); if (fgets(yesno,4,stdin)) { if (yesno[0]=='y') { @@ -581,11 +581,11 @@ int main(int argc, char* argv[]) } } else if (action==CONVERT_TO_FAT32) { if (!ipod.macpod) { - printf("[ERR] Ipod is already FAT32, aborting\n"); + printf("[ERR] iPod is already FAT32, aborting\n"); } else { printf("WARNING!!! YOU ARE ABOUT TO USE AN EXPERIMENTAL FEATURE.\n"); printf("ALL DATA ON YOUR IPOD WILL BE ERASED.\n"); - printf("Are you sure you want to convert your ipod to FAT32? (y/n):"); + printf("Are you sure you want to convert your iPod to FAT32? (y/n):"); if (fgets(yesno,4,stdin)) { if (yesno[0]=='y') { From 41be2016b8a2199d2bddb39b160d5df304c321a3 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Tue, 29 Sep 2026 23:02:42 +0300 Subject: [PATCH 315/408] docs: Fix case of "iPod" in area name and history Change-Id: Ie0f54f98f95a868642e95d957b25aaa651ae9545 --- docs/HISTORY | 8 ++++---- docs/MAINTAINERS | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/HISTORY b/docs/HISTORY index 3dcc704891..a53bb4c579 100644 --- a/docs/HISTORY +++ b/docs/HISTORY @@ -211,17 +211,17 @@ Rockbox History the LaTeX manual was born - ipod video audio playback + iPod video audio playback * March 2006 iAudio X5 bootloader - ipod Mini 2G audio + iPod Mini 2G audio * April 2006 - ipod Mini 1G audio + iPod Mini 1G audio * July 2006 @@ -231,7 +231,7 @@ Rockbox History Support for Toshiba Gigabeat F - Auto-detect 2048 bytes block-size to work fine on 5.5G ipod video + Auto-detect 2048 bytes block-size to work fine on 5.5G iPod video * January 2007 diff --git a/docs/MAINTAINERS b/docs/MAINTAINERS index da3ec2c371..3c983f0894 100644 --- a/docs/MAINTAINERS +++ b/docs/MAINTAINERS @@ -37,16 +37,16 @@ specific to that target. :Iriver H100/H115: Linus Nielsen Feltzing :Iriver H120/H140: Miika Pekkarinen, Linus Nielsen Feltzing :Iriver H320/H340: Peter D'Hoye -:Ipod 1st Gen: Jens Arnold -:Ipod 2nd Gen: Jens Arnold -:Ipod 3rd Gen: -:Ipod 4th Gen Grayscale: -:Ipod Color/Photo: Dave Chapman -:Ipod Video: -:Ipod Classic: -:Ipod Mini 1st Gen: -:Ipod Mini 2nd Gen: Jens Arnold -:Ipod Nano 1st Gen: Thom Johansen +:iPod 1st Gen: Jens Arnold +:iPod 2nd Gen: Jens Arnold +:iPod 3rd Gen: +:iPod 4th Gen Grayscale: +:iPod Color/Photo: Dave Chapman +:iPod Video: +:iPod Classic: +:iPod Mini 1st Gen: +:iPod Mini 2nd Gen: Jens Arnold +:iPod Nano 1st Gen: Thom Johansen :Iriver H10 20GB: Barry Wardell :Iriver H10 5GB/6GB: :Iaudio X5: Linus Nielsen Feltzing @@ -290,8 +290,8 @@ Bootloaders :HibyOS: Solomon Peachy :Iriver H1xx/H3xx: Miika Pekkarinen, Linus Nielsen Feltzing :Iaudio X5/M5: Linus Nielsen Feltzing -:PortalPlayer (not ipod): Barry Wardell -:Ipod: Dave Chapman +:PortalPlayer (not iPod): Barry Wardell +:iPod: Dave Chapman :Gigabeat F/X: Barry Wardell :xDuoo X3: Solomon Peachy :x1000: Aidan MacDonald From 23f1f294b5320dad8f5a7a5eb40ffee06f44b2be Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Tue, 29 Sep 2026 23:41:49 +0300 Subject: [PATCH 316/408] Fix case of "iPod" in docs and CLI tools help Change-Id: I50a6a8f9cac361eee1c36f66570b20b5521b3283 --- tools/scramble.c | 6 +++--- utils/ipod/flashsplit/README | 4 ++-- utils/mks5lboot/README | 2 +- utils/tools/bin2c.c | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/scramble.c b/tools/scramble.c index 85879e40f3..0d4bc69bd9 100644 --- a/tools/scramble.c +++ b/tools/scramble.c @@ -80,9 +80,9 @@ void usage(void) "\t-iaudiox5v iAudio X5V format\n" "\t-iaudiom5 iAudio M5 format\n" "\t-iaudiom3 iAudio M3 format\n"); - printf("\t-ipod3g ipod firmware partition format (3rd Gen)\n" - "\t-ipod4g ipod firmware partition format (4th Gen, Mini, Nano, Photo/Color)\n" - "\t-ipod5g ipod firmware partition format (5th Gen - aka Video)\n" + printf("\t-ipod3g iPod firmware partition format (3rd Gen)\n" + "\t-ipod4g iPod firmware partition format (4th Gen, Mini, Nano, Photo/Color)\n" + "\t-ipod5g iPod firmware partition format (5th Gen - aka Video)\n" "\t-creative=X [-no-ciff] Creative firmware structure format\n" "\t (X values: zenv, zen, zenxfi, zenmozaic)\n"); printf("\t-gigabeat Toshiba Gigabeat F/X format\n" diff --git a/utils/ipod/flashsplit/README b/utils/ipod/flashsplit/README index bca13f9c04..814a576ab0 100644 --- a/utils/ipod/flashsplit/README +++ b/utils/ipod/flashsplit/README @@ -1,8 +1,8 @@ flashsplit ---------- -A simple utility to split a dump of the NOR flash on an ipod. +A simple utility to split a dump of the NOR flash on an iPod. -Should work on all PortalPlayer-based ipods. +Should work on all PortalPlayer-based iPods. See http://www.rockbox.org/wiki/IpodFlash for more information. diff --git a/utils/mks5lboot/README b/utils/mks5lboot/README index 97c0b81b5a..f2893724ea 100644 --- a/utils/mks5lboot/README +++ b/utils/mks5lboot/README @@ -81,7 +81,7 @@ Prerequisites: Nano 3G 4GB: 1.0.1 to 1.1.3 - If your iPod is formated using Apple partitions you must convert this - ipod to FAT32 format (aka a "winpod"), see http://www.rockbox.org/ + iPod to FAT32 format (aka a "winpod"), see http://www.rockbox.org/ wiki/IpodConversionToFAT32 - It is recommended to install the RB firmware before installing the dual diff --git a/utils/tools/bin2c.c b/utils/tools/bin2c.c index 4600746d52..27101fff6e 100644 --- a/utils/tools/bin2c.c +++ b/utils/tools/bin2c.c @@ -42,7 +42,7 @@ static void usage(void) { fprintf(stderr, "bin2c [options] infile cfile\n"); - fprintf(stderr, " -i ipod mode\n"); + fprintf(stderr, " -i iPod mode\n"); } From 8dcf743951138371468f4302678a82c16266af4e Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 22:10:02 +0200 Subject: [PATCH 317/408] rk27xx: set the NAND bus timing from the access time flash_init() sets FMWAIT to 0x1081, as the OF's FlashInit() does, and nothing changed it after. But the OF, and the Samsung YP-CP3's NAND bootloader, follow it with FlashTimingCfg(), at every bus clock change: from chip 0's access time and the bus clock it derives a timing that, for the YP-CP3's 25 ns Samsung part at 100 MHz, is 0x60 - by the RK28 controller's register layout, under half the bus cycles per byte. Do the same once chips are detected, for 100 MHz: the AHB runs at CPUFREQ_MAX / 2 or slower, where the value only gains margin. Over USB mass storage on the YP-CP3 the NAND read at 5.70 MB/s and wrote at 3.26 MB/s; now 6.83 and 3.65 MB/s, every read verified, also after a power cycle. Co-Authored-By: Claude Opus 5.5 Change-Id: I356214c5d10e65952abc3334f5fae5deb2e02cff --- firmware/target/arm/rk27xx/nand-rk27xx.c | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/firmware/target/arm/rk27xx/nand-rk27xx.c b/firmware/target/arm/rk27xx/nand-rk27xx.c index 2cc1a348f8..a491d661bd 100644 --- a/firmware/target/arm/rk27xx/nand-rk27xx.c +++ b/firmware/target/arm/rk27xx/nand-rk27xx.c @@ -163,6 +163,43 @@ void flash_chip_select(uint8_t chip) FMCTL = tmp; } +/* NAND bus timing for an AHB clock of `mhz`, from chip 0's access time, as + * the OF's FlashTimingCfg() computes it - the OF and the YP-CP3's NAND + * bootloader call it at every bus clock change. The value starts at 0x40, + * or 0x41 on Hynix and 0x60 on Toshiba and Micron parts, and grows by 0x20 + * (by 1 from 0x100 on) for every bus cycle the access time needs beyond + * two. On the YP-CP3's Samsung part (25 ns) at 100 MHz that is 0x60, where + * flash_init()'s 0x1081 - what the OF's FlashInit() sets before - spends + * about twice as long on every byte. */ +static void flash_timing_cfg(uint32_t mhz) +{ + const struct flashspec_t *f = &flash_spec[0]; + uint32_t wait, period, cycles; + + if (mhz == 0 || mhz >= 200) + return; + + if (f->vendor == MICRON || f->vendor == TOSHIBA) + wait = 0x60; + else if (f->vendor == HYNIX) + wait = 0x41; + else + wait = 0x40; + + period = 1000 / mhz; + cycles = (f->access_time + period - 1) / period; + + while (cycles-- > 2) + { + if (wait < 0x100) + wait += 0x20; + else + wait += 1; + } + + FMWAIT = wait; +} + void flash_init(void) { uint8_t buff[5]; /* buff for CMD_READ_ID response */ @@ -311,6 +348,10 @@ void flash_init(void) total_phy_sec += flash_spec[i].total_phy_sec; } + /* for the fastest bus clock: AHB runs at CPUFREQ_MAX / 2 or slower */ + if (total_phy_sec != 0) + flash_timing_cfg(CPUFREQ_MAX / 2 / 1000000); + /* read ID block and propagate SysDiskCapacity and SysResBlocks */ } From 21d5dacdb8ef4ef755268bc7f7160a40c6340888 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 22:25:53 +0200 Subject: [PATCH 318/408] rk27xx: overlap NAND read transfers with the slot copies Every sector of a read was transferred from the chip into a controller slot and then copied out of it, the next transfer starting only after the copy. Start it before: it goes into the next slot, not the one being copied. On the Samsung YP-CP3, same test: reads at 8.25 MB/s, the original firmware's speed, every read verified, also after a power cycle. Co-Authored-By: Claude Opus 5.5 Change-Id: I53f77ea3c00bb449d772357e39602cca9c5cdba3 --- firmware/target/arm/rk27xx/flash-rk27xx.c | 33 ++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.c b/firmware/target/arm/rk27xx/flash-rk27xx.c index 0478f97bfe..c4ba1b6dee 100644 --- a/firmware/target/arm/rk27xx/flash-rk27xx.c +++ b/firmware/target/arm/rk27xx/flash-rk27xx.c @@ -259,19 +259,26 @@ static void latch_page(uint32_t row, uint32_t mode) BCHCTL = BCH_RST | mode; } -/* Transfer the next sector of the latched page through slot `slot` & 3. - * Returns the BCH status. */ -static uint32_t read_next_sector(uint32_t slot, uint8_t *data, uint8_t *meta) +/* Start the transfer of the next sector of the latched page into slot + * `slot` & 3. */ +static void kick_read(uint32_t slot) { - uint32_t st; - uint32_t buf = slot & 3; + FLCTL = FL_KICK_READ | ((slot & 3) << 3); +} - FLCTL = FL_KICK_READ | (buf << 3); +/* Wait for the last kicked transfer. Returns its BCH status. */ +static uint32_t wait_read(void) +{ while (!(FLCTL & FL_RDY)) { } + return BCHST; +} - st = BCHST; +/* Copy a transferred sector out of slot `slot` & 3. */ +static void copy_slot(uint32_t slot, uint8_t *data, uint8_t *meta) +{ + uint32_t buf = slot & 3; if (data) { @@ -283,7 +290,6 @@ static uint32_t read_next_sector(uint32_t slot, uint8_t *data, uint8_t *meta) memcpy(meta, (const void *)((uintptr_t)&SPARE_BUF + (buf << 4)), FLASH_META_SIZE); } - return st; } /* Read sectors [first, first + n) of raw page `row`. Returns 1 if any was @@ -295,6 +301,7 @@ static int read_raw_run(uint32_t row, uint32_t first, uint32_t n, int uncorrectable = 0; latch_page(row, mode); + kick_read(0); for (j = 0; j < first + n; j++) { @@ -304,7 +311,15 @@ static int read_raw_run(uint32_t row, uint32_t first, uint32_t n, : NULL; uint8_t *m = (wanted && meta) ? meta + (size_t)k * FLASH_META_SIZE : NULL; - uint32_t st = read_next_sector(j, d, m); + uint32_t st = wait_read(); + + /* the next sector transfers, into the next slot, while this one is + * copied out */ + if (j + 1 < first + n) + { + kick_read(j + 1); + } + copy_slot(j, d, m); if (wanted) { From 8eb47f474b29444a1f45c88c871e493692b4559f Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 22:59:14 +0200 Subject: [PATCH 319/408] rk27xx: copy a page of every plane at a time copy_sectors() copied one destination raw page at a time, so on a two-plane part every page of a copy took two programs and two tPROG. Its buffer already holds a page of every plane: copy that much at once, and flash_program() programs the planes together. Copies are most of the programs when the FTL closes blocks that random writes left part written. Over USB mass storage on a Samsung YP-CP3 a stress test that ran 66792 one-plane programs ran 692 now, with 43958 two-plane ones, and its program time fell from 85 s to 59 s. Co-Authored-By: Claude Opus 5.5 Change-Id: Icc60d250ecc75c83c2d817efb7678333bffe3137 --- firmware/target/arm/rk27xx/flash-rk27xx.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.c b/firmware/target/arm/rk27xx/flash-rk27xx.c index c4ba1b6dee..abb203175c 100644 --- a/firmware/target/arm/rk27xx/flash-rk27xx.c +++ b/firmware/target/arm/rk27xx/flash-rk27xx.c @@ -714,9 +714,9 @@ static int copy_sectors(uint32_t src, uint32_t dst, unsigned n, { while (k < n && ret == 0) { - /* one destination raw page at a time */ - uint32_t len = geo.sec_per_page_raw - - (dst + k) % geo.sec_per_page_raw; + /* one destination page of every plane at a time, for + * flash_program() to program the planes together */ + uint32_t len = geo.sec_per_page - (dst + k) % geo.sec_per_page; uint32_t i = 0; if (len > n - k) From 5249466cc4ec985d4f5161586dca32c6d09ef90b Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 1 Oct 2026 23:47:32 +0200 Subject: [PATCH 320/408] rk27xx: leave the NAND program running until the next access Every program waited out its tPROG, about 0.8 ms on the Samsung YP-CP3, before returning. Over USB mass storage that wait comes before the status of each write command goes back to the host, and so before the host sends the next one: nothing else runs during it. Return once the program is started, WP# still lifted, and finish it - wait, check the status, restore WP# - at the next chip access, or at flash_sync(), which ftl_sync() calls. A program's result then arrives with the next flash call; no caller checks flash_program()'s. A copy still reports its own programs, the last one included, as the FTL moves the data elsewhere when one fails. On the YP-CP3 the NAND wrote at 3.64 MB/s; now 4.37 MB/s, against 4.35 MB/s in the original firmware, every read verified, also after a power cycle. Co-Authored-By: Claude Opus 5.5 Change-Id: I8de01f8be052e5d3ff06f550c614865fb1946f05 --- firmware/target/arm/rk27xx/flash-rk27xx.c | 91 +++++++++++++++++------ firmware/target/arm/rk27xx/flash-rk27xx.h | 7 +- firmware/target/arm/rk27xx/ftl-rk27xx.c | 1 + 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.c b/firmware/target/arm/rk27xx/flash-rk27xx.c index abb203175c..b5debb0935 100644 --- a/firmware/target/arm/rk27xx/flash-rk27xx.c +++ b/firmware/target/arm/rk27xx/flash-rk27xx.c @@ -87,6 +87,8 @@ static uint32_t ecc_mode; /* BCHCTL mode bits: 0 or BCH_T14 */ static uint32_t boot_area = UINT32_MAX; static struct flash_stats stats; +static int prog_finish(void); + int flash_layer_init(void) { const struct flashspec_t *f = &flash_spec[0]; @@ -136,6 +138,7 @@ const struct flash_geometry *flash_get_geometry(void) void flash_set_writable(bool on) { + prog_finish(); writable = on; } @@ -245,6 +248,7 @@ static void send_row(uint32_t row) * `mode` (BCHCTL mode bits). */ static void latch_page(uint32_t row, uint32_t mode) { + prog_finish(); flash_chip_select(0); wait_flash_ready(); @@ -496,51 +500,81 @@ static void load_run(uint8_t cmd, const struct prog_run *r) } } -/* Program one raw page, or with `planes` 2 a page in each plane - r[0] in - * the first, r[1] the same page of the second - with one two-plane program - * as the OF's FlashProgEnhanced() does: 80h, the first page, 11h, tDBSY, - * 81h, the second page, 10h. The planes then share one tPROG. */ +/* A program is left running when prog_raw() returns - WP# lifted - and + * finished by prog_finish() at the next chip access or at flash_sync(): its + * tPROG then overlaps whatever the caller does next, most of all the USB + * transfer that follows the last page of a write. */ +static bool prog_pending; +static bool prog_failed; /* a finished program failed: sticky */ + +/* Wait for a pending program and check its status. Returns 1 if it + * failed. */ +static int prog_finish(void) +{ + int fail = 0; + + if (prog_pending) + { + prog_pending = false; + flash_chip_select(0); + fail = wait_ready_us(PROG_TIMEOUT_US) + || (read_status() & NAND_STATUS_FAIL); + flash_chip_deselect(); + FMCTL &= ~FM_PROTECT; + + if (fail) + { + stats.prog_failures++; + prog_failed = true; + } + } + return fail; +} + +int flash_sync(void) +{ + return prog_finish(); +} + +/* Start programming one raw page, or with `planes` 2 a page in each plane - + * r[0] in the first, r[1] the same page of the second - with one two-plane + * program as the OF's FlashProgEnhanced() does: 80h, the first page, 11h, + * tDBSY, 81h, the second page, 10h. The planes then share one tPROG. + * Returns 1 if the program before failed, or this one could not start. */ static int prog_raw(const struct prog_run *r, unsigned planes) { - int fail; + int fail = prog_finish(); flash_chip_select(0); FMCTL |= FM_PROTECT; /* lift WP# for this operation */ wait_flash_ready(); load_run(CMD_PROG_1ST, &r[0]); - fail = 0; if (planes == 2) { FLASH_CMD(0) = CMD_PROG_PLANE; - fail = wait_ready_us(PLANE_TIMEOUT_US); - if (!fail) + if (wait_ready_us(PLANE_TIMEOUT_US)) { - load_run(CMD_PROG_1ST_P1, &r[1]); + flash_chip_deselect(); + FMCTL &= ~FM_PROTECT; + stats.prog_failures++; + prog_failed = true; + return 1; } + load_run(CMD_PROG_1ST_P1, &r[1]); } - if (!fail) - { - FLASH_CMD(0) = CMD_PROG_2ND; - fail = wait_ready_us(PROG_TIMEOUT_US) - || (read_status() & NAND_STATUS_FAIL); - } - + FLASH_CMD(0) = CMD_PROG_2ND; + prog_pending = true; flash_chip_deselect(); - FMCTL &= ~FM_PROTECT; - - if (fail) - { - stats.prog_failures++; - } - return fail ? 1 : 0; + return fail; } static int erase_raw_block(uint32_t row) { int fail; + prog_finish(); flash_chip_select(0); FMCTL |= FM_PROTECT; wait_flash_ready(); @@ -712,6 +746,11 @@ static int copy_sectors(uint32_t src, uint32_t dst, unsigned n, } else { + /* a copy reports its own programs' results, the last one included: + * the FTL moves the data elsewhere when one fails */ + prog_finish(); + prog_failed = false; + while (k < n && ret == 0) { /* one destination page of every plane at a time, for @@ -773,6 +812,12 @@ static int copy_sectors(uint32_t src, uint32_t dst, unsigned n, } k += len; } + + prog_finish(); + if (prog_failed) + { + ret = 1; + } } return ret; } diff --git a/firmware/target/arm/rk27xx/flash-rk27xx.h b/firmware/target/arm/rk27xx/flash-rk27xx.h index ed99cf7d9e..eacfe456d7 100644 --- a/firmware/target/arm/rk27xx/flash-rk27xx.h +++ b/firmware/target/arm/rk27xx/flash-rk27xx.h @@ -89,9 +89,14 @@ int flash_read(uint32_t sec, void *data, void *meta, unsigned n); /* Program n sectors from sec, which must be erased. data NULL programs * 0xff; meta NULL programs {0xff, 0x00, 0xff} - {0xff, 0xff, 0xff} with - * passthrough on. Returns 0 or 1 on failure. */ + * passthrough on. The last program is left running: its result arrives + * with the next flash call - a program returns 1 if the one before failed - + * or with flash_sync(). Returns 0 or 1 on failure. */ int flash_program(uint32_t sec, const void *data, const void *meta, unsigned n); +/* Wait for the program left running, if any. Returns 1 if it failed. */ +int flash_sync(void); + /* Program the whole page (every plane) that contains sec. */ int flash_program_page(uint32_t sec, const void *data, const void *meta); diff --git a/firmware/target/arm/rk27xx/ftl-rk27xx.c b/firmware/target/arm/rk27xx/ftl-rk27xx.c index 1f5de9ca3f..ce3d4ae5d9 100644 --- a/firmware/target/arm/rk27xx/ftl-rk27xx.c +++ b/firmware/target/arm/rk27xx/ftl-rk27xx.c @@ -345,5 +345,6 @@ uint32_t ftl_sync(void) { drive_sync(); } + flash_sync(); return 0; } From 1488b30c62b4843d0ca1aa019516408b1ba36b23 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Wed, 30 Sep 2026 09:28:49 -0400 Subject: [PATCH 321/408] imageviewer/jpeg: reject damaged files safely Damaged and truncated JPEGs could make the image viewer read or write outside its buffers: - process_markers() trusted segment lengths, and Huffman table symbol counts, so a segment running past the end of the file was parsed from whatever memory followed it. Check that each marker segment, and each Huffman table in it, lies within the file. - A file without a complete SOS header was decoded from a NULL entropy data pointer, as load_image() checked only for DQT and SOF. Require SOS as well. - img_mem() computed the image size in an int, which overflows for a large image (a 65535x65535 file came out as 0), so the decode wrote far past the buffer. Compute it in 64 bits and saturate. Found with the jpeg-conformance files of the imazen codec-corpus, which include truncated files and files from fuzzing. Co-Authored-By: Claude Opus 5.5 Change-Id: I1e2a9a4346fd1c0e34b0813267c6317dbf704bc7 --- apps/plugins/imageviewer/jpeg/jpeg.c | 13 +++++++------ apps/plugins/imageviewer/jpeg/jpeg_decoder.c | 19 ++++++++++++++++--- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/plugins/imageviewer/jpeg/jpeg.c b/apps/plugins/imageviewer/jpeg/jpeg.c index 2436e90f68..ea13ca7512 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg.c +++ b/apps/plugins/imageviewer/jpeg/jpeg.c @@ -91,21 +91,22 @@ static void draw_image_rect(struct image_info *info, static int img_mem(int ds) { - int size; + /* in 64 bits: a huge image overflows an int and would seem to fit */ + long long size; struct jpeg *p_jpg = &jpg; - size = (p_jpg->x_phys/ds/p_jpg->subsample_x[0]) + size = (long long)(p_jpg->x_phys/ds/p_jpg->subsample_x[0]) * (p_jpg->y_phys/ds/p_jpg->subsample_y[0]); #ifdef HAVE_LCD_COLOR if (p_jpg->blocks > 1) /* colour, add requirements for chroma */ { - size += (p_jpg->x_phys/ds/p_jpg->subsample_x[1]) + size += (long long)(p_jpg->x_phys/ds/p_jpg->subsample_x[1]) * (p_jpg->y_phys/ds/p_jpg->subsample_y[1]); - size += (p_jpg->x_phys/ds/p_jpg->subsample_x[2]) + size += (long long)(p_jpg->x_phys/ds/p_jpg->subsample_x[2]) * (p_jpg->y_phys/ds/p_jpg->subsample_y[2]); } #endif - return size; + return size > INT_MAX ? INT_MAX : size; } static int load_image(char *filename, struct image_info *info, @@ -177,7 +178,7 @@ static int load_image(char *filename, struct image_info *info, /* process markers, unstuffing */ status = process_markers(buf_jpeg, filesize, p_jpg); - if (status < 0 || (status & (DQT | SOF0)) != (DQT | SOF0)) + if (status < 0 || (status & (DQT | SOF0 | SOS)) != (DQT | SOF0 | SOS)) { /* bad format or minimum components not contained */ #ifndef HAVE_LCD_COLOR rb->splashf(HZ, "unsupported %d", status); diff --git a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c index bc7e03f50e..ee4f3bcf62 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c +++ b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c @@ -494,12 +494,23 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) while (p_src < p_end) { + int marker; if (*p_src++ != 0xFF) /* no marker? */ { continue; /* discard */ } + if (p_src >= p_end) + break; + marker = *p_src++; + if (marker != 0xFF && marker != 0x00 && marker != 0x01 + && (marker < 0xD0 || marker > 0xD9)) + { /* a marker segment: it must lie within the file */ + if (p_end - p_src < 2 || (p_src[0] << 8 | p_src[1]) < 2 + || (p_src[0] << 8 | p_src[1]) > p_end - p_src) + return -13; /* segment runs past the end of the file */ + } - switch (*p_src++) + switch (marker) { case 0xFF: /* Previous FF was fill byte */ p_src--; /* This FF could be start of a marker */ @@ -599,7 +610,8 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) sum += *p_src; p_jpeg->hufftable[i].huffmancodes_ac[j] = *p_src++; } - if(16 + sum > AC_LEN) + if(16 + sum > AC_LEN + || p_src + sum > p_temp + marker_size - 2) return -10; /* longer than allowed */ for (; j < 16 + sum; j++) @@ -612,7 +624,8 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) sum += *p_src; p_jpeg->hufftable[i].huffmancodes_dc[j] = *p_src++; } - if(16 + sum > DC_LEN) + if(16 + sum > DC_LEN + || p_src + sum > p_temp + marker_size - 2) return -11; /* longer than allowed */ for (; j < 16 + sum; j++) From 5c5dac2dbcc123fdf98a59a2ab2678808b3033ad Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 2 Oct 2026 10:30:58 +0200 Subject: [PATCH 322/408] rk27xx: clip LCD updates to the screen lcd_update_rect() took the rect as given. One reaching past the screen set a GRAM window off the panel, so the update showed nothing, and built one DMA descriptor per line into scr_llp[LCD_HEIGHT] - past its end for a rect taller than what is left of the screen. What follows scr_llp in memory is the PCM driver's locks and then all_queues, the kernel's queue list: a later broadcast posted to garbage. invadrox asks for such a rect every frame. On a Samsung YP-CP3 nothing of its playfield moved - aliens, bombs, the ship - and powering off afterwards took a data abort in queue_post() from interrupt context. Clip the rect to the screen, as other targets do, and do nothing if nothing is left. Every rk27xx screen is a multiple of 4 pixels each way, so aligning the clipped rect to 4 cannot take it past the edge. Co-Authored-By: Claude Opus 5.5 Change-Id: I1f8bf865251859cd223e208a807c9a9da76a0ba0 --- firmware/target/arm/rk27xx/lcdif-rk27xx.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/firmware/target/arm/rk27xx/lcdif-rk27xx.c b/firmware/target/arm/rk27xx/lcdif-rk27xx.c index 618b476480..c023482679 100644 --- a/firmware/target/arm/rk27xx/lcdif-rk27xx.c +++ b/firmware/target/arm/rk27xx/lcdif-rk27xx.c @@ -258,6 +258,27 @@ void lcd_update_rect(int x, int y, int width, int height) { int x_end, y_end, x_align, y_align; + /* Clip to the screen, as other targets do. A rect reaching past it + * set a GRAM window off the panel - nothing showed - and built more + * DMA descriptors than scr_llp[] holds, overwriting what follows it + * in memory: the kernel's queue list among it. */ + if (x < 0) + { + width += x; + x = 0; + } + if (y < 0) + { + height += y; + y = 0; + } + if (width > LCD_WIDTH - x) + width = LCD_WIDTH - x; + if (height > LCD_HEIGHT - y) + height = LCD_HEIGHT - y; + if (width <= 0 || height <= 0) + return; + /* min alowed transfer seems to be 4x4 pixels */ x_align = x & 3; y_align = y & 3; From 3fa27091f3144ba7bcc9851279e5a33cfcad1870 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 2 Oct 2026 10:30:58 +0200 Subject: [PATCH 323/408] rk27xx: wait for the LCD DMA transfer to end lcd_update_rect() waited on the channel's CTL_L LLP_DST_EN bit, which the last descriptor has clear: it clears when the last block is loaded, not when it is done. The update returned with that line still being read, and the next one reprogrammed the window and the channel under it. Wait for the channel to disable itself, which it does after its last block. Co-Authored-By: Claude Opus 5.5 Change-Id: Icf9314fbe40b8ea53a7f5f24ea11513e87a54cf6 --- firmware/target/arm/rk27xx/lcdif-rk27xx.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/firmware/target/arm/rk27xx/lcdif-rk27xx.c b/firmware/target/arm/rk27xx/lcdif-rk27xx.c index c023482679..bb3500d866 100644 --- a/firmware/target/arm/rk27xx/lcdif-rk27xx.c +++ b/firmware/target/arm/rk27xx/lcdif-rk27xx.c @@ -305,8 +305,11 @@ void lcd_update_rect(int x, int y, int width, int height) /* Setup buffered writes to lcd controler */ MCU_CTRL = MCU_CTRL_RS_HIGH|MCU_CTRL_BUFF_WRITE|MCU_CTRL_BUFF_START; - /* Wait for DMA transfer to finish */ - while (DWDMA_CTL_L(0) & (1<<27)); + /* Wait for the DMA transfer to finish: the channel disables itself + * after its last block. CTL_L's LLP_DST_EN (bit 27) is no measure - + * it clears as soon as the last block is loaded, while that block + * still runs. */ + while (DWDMA_DMA_CHEN & (1<<0)); } void lcd_update() From 69af17695d621aadfdfba5dc7f69edde369e2c81 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 2 Oct 2026 10:30:58 +0200 Subject: [PATCH 324/408] rk27xx: keep the LCD bus timing at the boosted clock The LCD controller's CSn/WEn/RDn strobes count bus clocks, set once for 1-4-1 clocks: a 120 ns write cycle at the 50 MHz bus clock, but 60 ns when the CPU is boosted and the bus runs at 100 MHz. That is too fast for the Samsung YP-CP3's panel: with the CPU boosted it showed stray pixels, and partial updates left tearing behind moving things - the boot logo too, as the firmware boosts before lcd_init(). Double the clocks while boosted, 2-8-2, which keeps the write cycle at 120 ns: set_cpu_frequency() switches them before raising the clock and after lowering it, and lcd init picks them for the clock it runs at - the bootloader stays at crt0's CPUFREQ_MAX. Co-Authored-By: Claude Opus 5.5 Change-Id: I482073c16af39c6b1856a0e18d19a747c78975f0 --- firmware/target/arm/rk27xx/lcdif-rk27xx.c | 17 ++++++++++++++++- firmware/target/arm/rk27xx/lcdif-rk27xx.h | 5 +++++ firmware/target/arm/rk27xx/system-rk27xx.c | 6 ++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/firmware/target/arm/rk27xx/lcdif-rk27xx.c b/firmware/target/arm/rk27xx/lcdif-rk27xx.c index bb3500d866..8a1927242b 100644 --- a/firmware/target/arm/rk27xx/lcdif-rk27xx.c +++ b/firmware/target/arm/rk27xx/lcdif-rk27xx.c @@ -95,6 +95,19 @@ static void lcdctrl_buff_setup(int width, int height) LINE3_UVADDR = LINE3_YADDR + 1; } +/* CSn/WEn/RDn timings in MCU mode, in AHB clocks: csrw (bits 9-7), rwpw + * (6-3), rwcs (2-0). csrw 1, rwpw 4, rwcs 1 makes a 120 ns write cycle at + * the 50 MHz bus clock, but 60 ns at the boosted 100 MHz - too fast for + * the panel: stray pixels, and partial updates landing in the wrong place. + * Twice the clocks keep it 120 ns there. */ +#define LCD_TIMING_50MHZ ((1<<7) | (4<<3) | 1) +#define LCD_TIMING_100MHZ ((2<<7) | (8<<3) | 2) + +void lcdif_set_bus_timing(bool boosted) +{ + VERT_PERIOD = boosted ? LCD_TIMING_100MHZ : LCD_TIMING_50MHZ; +} + static void lcdctrl_init(void) { int i; @@ -107,7 +120,9 @@ static void lcdctrl_init(void) LCDC_CTRL = ALPHA(7) | LCDC_STOP | LCDC_MCU | RGB24B; MCU_CTRL = ALPHA_BASE(0x3f) | MCU_CTRL_BYPASS; - VERT_PERIOD = (1<<7)|(1<<5)|1; /* CSn/WEn/RDn signal timings */ + /* for the clock running now: the firmware boosts before lcd_init(), + * and the bootloader runs at crt0's, which is CPUFREQ_MAX */ + lcdif_set_bus_timing(cpu_frequency == CPUFREQ_MAX); lcd_display_init(); lcdctrl_bypass(0); diff --git a/firmware/target/arm/rk27xx/lcdif-rk27xx.h b/firmware/target/arm/rk27xx/lcdif-rk27xx.h index 4961863504..8461c7d6a9 100644 --- a/firmware/target/arm/rk27xx/lcdif-rk27xx.h +++ b/firmware/target/arm/rk27xx/lcdif-rk27xx.h @@ -1,6 +1,8 @@ #ifndef _LCDIF_RK27XX_H #define _LCDIF_RK27XX_H +#include + #define LCDIF_16BIT 16 #define LCDIF_18BIT 18 @@ -12,4 +14,7 @@ void lcd_display_init(void); void lcd_set_gram_area(int x_start, int y_start, int x_end, int y_end); +/* LCD bus timing for the bus clock: set before it rises, after it falls */ +void lcdif_set_bus_timing(bool boosted); + #endif /* _LCDIF_RK27XX_H */ diff --git a/firmware/target/arm/rk27xx/system-rk27xx.c b/firmware/target/arm/rk27xx/system-rk27xx.c index 729b0c25a6..0d4a521cf8 100644 --- a/firmware/target/arm/rk27xx/system-rk27xx.c +++ b/firmware/target/arm/rk27xx/system-rk27xx.c @@ -23,6 +23,7 @@ #include "panic.h" #include "button.h" #include "system-target.h" +#include "lcdif-rk27xx.h" #define default_interrupt(name) \ extern __attribute__((weak,alias("UIRQ"))) void name (void) @@ -249,6 +250,9 @@ void set_cpu_frequency(long frequency) if (frequency == CPUFREQ_MAX) { + /* the LCD strobes take twice the clocks at twice the clock */ + lcdif_set_bus_timing(true); + /* PLL set to 200 Mhz * PLL:ARM = 1:1 * ARM:AHB = 2:1 @@ -282,6 +286,8 @@ void set_cpu_frequency(long frequency) SCU_DIVCON1 &= ~1; set_sdram_timing(CPUFREQ_NORMAL); + + lcdif_set_bus_timing(false); } cpu_frequency = frequency; From d94e41bade97756046f7c3b379ef65dbb7f46e14 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 2 Oct 2026 10:30:58 +0200 Subject: [PATCH 325/408] invadrox: bracket SCORENUM_Y Five layouts define SCORENUM_Y as SCORE_Y + (...) unbracketed, so the playfield update after each frame, PLAYFIELD_Y + 1 - SCORENUM_Y - FONT_HEIGHT high, added that part rather than subtracting it: on a 240-line screen it ran 25 lines past the bottom. Most LCD drivers clip it; the rk27xx one did not, and nothing in the playfield moved. Co-Authored-By: Claude Opus 5.5 Change-Id: Icecf7780d81595db9d6e42125fc421cc1cb40339 --- apps/plugins/invadrox.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/plugins/invadrox.c b/apps/plugins/invadrox.c index 3b12307d0e..40b0967dce 100644 --- a/apps/plugins/invadrox.c +++ b/apps/plugins/invadrox.c @@ -431,7 +431,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define SHIP_Y (PLAYFIELD_Y - 3 * SHIP_HEIGHT) #define ALIEN_START_Y (UFO_Y + 3 * ALIEN_HEIGHT) #define SCORENUM_X (PLAYFIELD_X + NUMBERS_WIDTH) -#define SCORENUM_Y SCORE_Y + (2 * (FONT_HEIGHT + 1) + 1) +#define SCORENUM_Y (SCORE_Y + 2 * (FONT_HEIGHT + 1) + 1) #define HISCORENUM_X (LCD_WIDTH - PLAYFIELD_X - 1 - 6 * NUMBERS_WIDTH - 5 * NUM_SPACING) #define SHIELD_Y (PLAYFIELD_Y - 6 * SHIP_HEIGHT) #define LIVES_X 10 @@ -455,7 +455,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define SHIP_Y (PLAYFIELD_Y - 3 * SHIP_HEIGHT) #define ALIEN_START_Y (UFO_Y + 3 * ALIEN_HEIGHT) #define SCORENUM_X (PLAYFIELD_X + NUMBERS_WIDTH) + 10 -#define SCORENUM_Y SCORE_Y + (2 * (FONT_HEIGHT + 1) + 1) +#define SCORENUM_Y (SCORE_Y + 2 * (FONT_HEIGHT + 1) + 1) #define HISCORENUM_X (LCD_WIDTH - PLAYFIELD_X - 1 - 6 * NUMBERS_WIDTH - 8 * NUM_SPACING) #define SHIELD_Y (PLAYFIELD_Y - 6 * SHIP_HEIGHT) #define LIVES_X 10 @@ -645,7 +645,7 @@ CONFIG_KEYPAD == MROBE500_PAD #undef SCORE_Y #define SCORE_Y 80 #define SCORENUM_X (PLAYFIELD_X + NUMBERS_WIDTH) -#define SCORENUM_Y SCORE_Y + (2 * (FONT_HEIGHT + 1) + 1) +#define SCORENUM_Y (SCORE_Y + 2 * (FONT_HEIGHT + 1) + 1) #define HISCORENUM_X (LCD_WIDTH - PLAYFIELD_X - 1 - 6 * NUMBERS_WIDTH - 5 * NUM_SPACING) #define SHIELD_Y (PLAYFIELD_Y - 6 * SHIP_HEIGHT) #define LIVES_X 10 @@ -699,7 +699,7 @@ CONFIG_KEYPAD == MROBE500_PAD #undef SCORE_Y #define SCORE_Y 80 #define SCORENUM_X (PLAYFIELD_X + NUMBERS_WIDTH) -#define SCORENUM_Y SCORE_Y + (2 * (FONT_HEIGHT + 1) + 1) +#define SCORENUM_Y (SCORE_Y + 2 * (FONT_HEIGHT + 1) + 1) #define HISCORENUM_X (LCD_WIDTH - PLAYFIELD_X - 1 - 6 * NUMBERS_WIDTH - 5 * NUM_SPACING) #define SHIELD_Y (PLAYFIELD_Y - 6 * SHIP_HEIGHT) #define LIVES_X 10 @@ -717,7 +717,7 @@ CONFIG_KEYPAD == MROBE500_PAD #undef SCORE_Y #define SCORE_Y 80 #define SCORENUM_X (PLAYFIELD_X + NUMBERS_WIDTH) -#define SCORENUM_Y SCORE_Y + (2 * (FONT_HEIGHT + 1) + 1) +#define SCORENUM_Y (SCORE_Y + 2 * (FONT_HEIGHT + 1) + 1) #define HISCORENUM_X (LCD_WIDTH - PLAYFIELD_X - 1 - 6 * NUMBERS_WIDTH - 5 * NUM_SPACING) #define SHIELD_Y (PLAYFIELD_Y - 6 * SHIP_HEIGHT) #define LIVES_X 10 From 02f54fc3a86a2d8ade7255af2daf7109f5a9b9a3 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 2 Oct 2026 00:24:12 +0200 Subject: [PATCH 326/408] plugins: 400x240 landscape screens Seven plugins have no layout or bitmaps for a 400x240 landscape screen - the Samsung YP-CP3's, which no target building plugins has had in that orientation. Give each the 320x240 one: the same height, and centred in the 80 pixels more width wherever a 320x240 background has to line up with it. - bubbles, invadrox, rockblox: the 320x240 layout and background, centred; the margins are cleared - sudoku, jewels: the 320x240 bitmaps; their layouts already centre themselves or use the width - superdom: the 320x240 box size and board items - boxes as wide as the screen allows make the board taller than it - wormlet: the sizes of 320x240 Co-Authored-By: Claude Opus 5.5 Change-Id: I223154da5da2250ba0f6068106ba9bec242a29ee --- apps/plugins/bitmaps/mono/SOURCES | 1 + apps/plugins/bitmaps/native/SOURCES | 10 ++++++---- apps/plugins/bubbles.c | 27 ++++++++++++++++++++++++++- apps/plugins/invadrox.c | 15 +++++++++++---- apps/plugins/rockblox.c | 28 ++++++++++++++++++++++------ apps/plugins/sudoku/sudoku.c | 3 ++- apps/plugins/superdom.c | 8 +++++++- apps/plugins/wormlet.c | 2 +- 8 files changed, 76 insertions(+), 18 deletions(-) diff --git a/apps/plugins/bitmaps/mono/SOURCES b/apps/plugins/bitmaps/mono/SOURCES index d864ef5ea7..82f42ec3b1 100644 --- a/apps/plugins/bitmaps/mono/SOURCES +++ b/apps/plugins/bitmaps/mono/SOURCES @@ -20,6 +20,7 @@ bubbles_bubble.138x110x1.bmp bubbles_bubble.220x176x1.bmp #elif ((LCD_WIDTH == 240) && (LCD_HEIGHT == 240)) || \ ((LCD_WIDTH == 320) && (LCD_HEIGHT == 240)) || \ + ((LCD_WIDTH == 400) && (LCD_HEIGHT == 240)) || \ ((LCD_WIDTH == 240) && (LCD_HEIGHT >= 320)) || \ ((LCD_WIDTH == 360) && (LCD_HEIGHT == 400)) || \ ((LCD_WIDTH == 360) && (LCD_HEIGHT == 640)) diff --git a/apps/plugins/bitmaps/native/SOURCES b/apps/plugins/bitmaps/native/SOURCES index e9c852e1b2..da32862caf 100644 --- a/apps/plugins/bitmaps/native/SOURCES +++ b/apps/plugins/bitmaps/native/SOURCES @@ -440,7 +440,7 @@ invadrox_numbers.50x7x16.bmp invadrox_background.480x640x16.bmp #elif LCD_WIDTH == 360 && (LCD_HEIGHT == 400 || LCD_HEIGHT == 640) invadrox_background.360x400x16.bmp -#elif LCD_WIDTH == 320 +#elif LCD_WIDTH == 320 || (LCD_WIDTH == 400 && LCD_HEIGHT == 240) invadrox_background.320x240x16.bmp #elif LCD_WIDTH == 240 && LCD_HEIGHT == 240 invadrox_background.240x240x16.bmp @@ -511,7 +511,7 @@ jewels.220x176x16.bmp jewels.240x240x16.bmp #elif (LCD_WIDTH == 240) && (LCD_HEIGHT >= 320) jewels.320x240x16.bmp -#elif (LCD_WIDTH == 320) && (LCD_HEIGHT == 240) +#elif ((LCD_WIDTH == 320) || (LCD_WIDTH == 400)) && (LCD_HEIGHT == 240) jewels.320x240x16.bmp #elif (LCD_WIDTH == 360) && ((LCD_HEIGHT == 400) || (LCD_HEIGHT == 640)) jewels.360x400x16.bmp @@ -909,7 +909,8 @@ sudoku_inverse.240x240x16.bmp sudoku_start.320x240x16.bmp sudoku_normal.320x240x16.bmp sudoku_inverse.320x240x16.bmp -#elif (LCD_WIDTH == 320) && (LCD_HEIGHT == 240) && (LCD_DEPTH >= 16) +#elif ((LCD_WIDTH == 320) || (LCD_WIDTH == 400)) && (LCD_HEIGHT == 240) \ + && (LCD_DEPTH >= 16) sudoku_start.320x240x16.bmp sudoku_normal.320x240x16.bmp sudoku_inverse.320x240x16.bmp @@ -943,7 +944,8 @@ superdom_boarditems.220x176x16.bmp ((LCD_WIDTH == 176) && (LCD_HEIGHT == 220)) superdom_boarditems.176x132x16.bmp #elif ((LCD_WIDTH == 240) && (LCD_HEIGHT == 240)) || \ - ((LCD_WIDTH == 320) && (LCD_HEIGHT == 240)) + ((LCD_WIDTH == 320) && (LCD_HEIGHT == 240)) || \ + ((LCD_WIDTH == 400) && (LCD_HEIGHT == 240)) superdom_boarditems.320x240x16.bmp #elif ((LCD_WIDTH == 240) && (LCD_HEIGHT == 320)) || \ ((LCD_WIDTH == 240) && (LCD_HEIGHT == 400)) || \ diff --git a/apps/plugins/bubbles.c b/apps/plugins/bubbles.c index 374b4cad38..5774fa089a 100644 --- a/apps/plugins/bubbles.c +++ b/apps/plugins/bubbles.c @@ -170,6 +170,23 @@ enum { #define NEXT_BB_WIDTH 32 #define NEXT_BB_Y 200 +/* 22x22 bubbles: the 320x240 layout, centred (Samsung YP-CP3) */ +#elif (LCD_HEIGHT == 240) && (LCD_WIDTH == 400) +#define LAYOUT_X ((LCD_WIDTH - 320) / 2) +#define XOFS (LAYOUT_X + 72) +#define MAX_FPS 40 + +/* the 320x240 text positions, which centre on its left panel */ +#define LEVEL_TXT_X LAYOUT_X +#define LEVEL_TXT_WIDTH 72 +#define LEVEL_TXT_Y 2 +#define SCORE_TXT_X LAYOUT_X +#define SCORE_TXT_WIDTH 72 +#define SCORE_TXT_Y 29 +#define NEXT_BB_X (LAYOUT_X + 20) +#define NEXT_BB_WIDTH 32 +#define NEXT_BB_Y 200 + /* 22x22 bubbles (Gigabeat, Onda VX747) */ #elif ((LCD_HEIGHT == 320) || (LCD_HEIGHT == 400)) && (LCD_WIDTH == 240) #define XOFS 64 @@ -1524,7 +1541,9 @@ static void bubbles_drawboard(struct game_context* bb) { /* clear screen */ rb->lcd_clear_display(); -#if ((LCD_HEIGHT >= 128) && (LCD_WIDTH <= 320)) || ((LCD_WIDTH == 132) && (LCD_HEIGHT == 80)) +#if ((LCD_HEIGHT >= 128) && (LCD_WIDTH <= 320)) || \ + ((LCD_WIDTH == 132) && (LCD_HEIGHT == 80)) || \ + ((LCD_WIDTH == 400) && (LCD_HEIGHT == 240)) h = rb->font_get(FONT_SYSFIXED)->height + 1; #elif (LCD_WIDTH == 640) && (LCD_HEIGHT == 480) h = rb->font_get(FONT_SYSFIXED)->height + 6; @@ -1534,7 +1553,13 @@ static void bubbles_drawboard(struct game_context* bb) { #endif /* draw background */ #ifdef HAVE_LCD_COLOR +#ifdef LAYOUT_X + /* the layout's background, centred */ + rb->lcd_bitmap(bubbles_background, LAYOUT_X, 0, + LCD_WIDTH - 2 * LAYOUT_X, LCD_HEIGHT); +#else rb->lcd_bitmap(bubbles_background, 0, 0, LCD_WIDTH, LCD_HEIGHT); +#endif #endif /* display play board */ diff --git a/apps/plugins/invadrox.c b/apps/plugins/invadrox.c index 40b0967dce..7553ba8537 100644 --- a/apps/plugins/invadrox.c +++ b/apps/plugins/invadrox.c @@ -395,8 +395,8 @@ CONFIG_KEYPAD == MROBE500_PAD #define LIVES_X 10 #define MAX_Y 18 -/* iPod Video defines */ -#elif (LCD_WIDTH == 320) && (LCD_HEIGHT == 240) +/* iPod Video defines - and Samsung YP-CP3, 400x240: the same, centred */ +#elif ((LCD_WIDTH == 320) || (LCD_WIDTH == 400)) && (LCD_HEIGHT == 240) /* Original arcade game size 224x240, 1bpp with * red overlay at top and green overlay at bottom. @@ -427,7 +427,7 @@ CONFIG_KEYPAD == MROBE500_PAD */ #define ARCADISH_GRAPHICS -#define PLAYFIELD_X 48 +#define PLAYFIELD_X (48 + (LCD_WIDTH - 320) / 2) #define SHIP_Y (PLAYFIELD_Y - 3 * SHIP_HEIGHT) #define ALIEN_START_Y (UFO_Y + 3 * ALIEN_HEIGHT) #define SCORENUM_X (PLAYFIELD_X + NUMBERS_WIDTH) @@ -1633,8 +1633,15 @@ static void move_ufo(void) static void draw_background(void) { - +#if BMPWIDTH_invadrox_background < LCD_WIDTH + /* a narrower background, centred like the playfield */ + rb->lcd_clear_display(); + rb->lcd_bitmap(invadrox_background, + (LCD_WIDTH - BMPWIDTH_invadrox_background) / 2, 0, + BMPWIDTH_invadrox_background, LCD_HEIGHT); +#else rb->lcd_bitmap(invadrox_background, 0, 0, LCD_WIDTH, LCD_HEIGHT); +#endif rb->lcd_update(); } diff --git a/apps/plugins/rockblox.c b/apps/plugins/rockblox.c index ac23ff7b6f..e674ec3c03 100644 --- a/apps/plugins/rockblox.c +++ b/apps/plugins/rockblox.c @@ -609,15 +609,17 @@ #define PREVIEW_X 258 #define PREVIEW_Y 300 -#elif (LCD_WIDTH == 320) && (LCD_HEIGHT == 240) +#elif ((LCD_WIDTH == 320) || (LCD_WIDTH == 400)) && (LCD_HEIGHT == 240) +/* 400x240: the 320x240 layout, centred */ +#define LAYOUT_X ((LCD_WIDTH - 320) / 2) #define BLOCK_WIDTH 12 #define BLOCK_HEIGHT 12 -#define BOARD_X 86 +#define BOARD_X (LAYOUT_X + 86) #define BOARD_Y 0 -#define PREVIEW_X 12 +#define PREVIEW_X (LAYOUT_X + 12) #define PREVIEW_Y 11 -#define LABEL_X 242 +#define LABEL_X (LAYOUT_X + 242) #define SCORE_Y 25 #define LEVEL_Y 70 #define LINES_Y 105 @@ -835,6 +837,20 @@ extern const fb_data rockblox_background[]; +#ifndef LAYOUT_X +#define LAYOUT_X 0 +#endif + +/* the background, which spans the layout - not the screen, when the + * layout is a smaller one centred */ +static void draw_background(void) +{ + if (LAYOUT_X > 0) + rb->lcd_clear_display(); + rb->lcd_bitmap(rockblox_background, LAYOUT_X, 0, + LCD_WIDTH - 2 * LAYOUT_X, LCD_HEIGHT); +} + #ifndef HIGHSCORE_SPACE #define HIGHSCORE_SPACE " " #endif @@ -1115,7 +1131,7 @@ static void init_rockblox (bool resume) tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); - rb->lcd_bitmap (rockblox_background, 0, 0, LCD_WIDTH, LCD_HEIGHT); + draw_background(); if (!resume) { rockblox_status.level = 1; @@ -1583,7 +1599,7 @@ static int rockblox_loop (void) backlight_ignore_timeout(); /* get rid of the splash text */ - rb->lcd_bitmap (rockblox_background, 0, 0, LCD_WIDTH, LCD_HEIGHT); + draw_background(); show_details (); #ifdef HIGH_SCORE_Y show_highscores (); diff --git a/apps/plugins/sudoku/sudoku.c b/apps/plugins/sudoku/sudoku.c index 3f192e9f0c..22e1ac50f6 100644 --- a/apps/plugins/sudoku/sudoku.c +++ b/apps/plugins/sudoku/sudoku.c @@ -146,8 +146,9 @@ static const char default_game[9][9] = #define MARK_SPACE 1 /* Pixels between two marks */ #define MARK_SIZE 4 /* Mark width and height */ -#elif (LCD_HEIGHT==240) && (LCD_WIDTH==320) +#elif (LCD_HEIGHT==240) && ((LCD_WIDTH==320) || (LCD_WIDTH==400)) /* iPod Video - 320x240, 9 cells @ 24x24 with 14 border lines */ +/* Samsung YP-CP3 - 400x240, the same, centred */ #define MARK_OFFS 1 /* Pixels between border and mark */ #define MARK_SPACE 2 /* Pixels between two marks */ #define MARK_SIZE 6 /* Mark width and height */ diff --git a/apps/plugins/superdom.c b/apps/plugins/superdom.c index 03d6c039eb..46ba5b1d15 100644 --- a/apps/plugins/superdom.c +++ b/apps/plugins/superdom.c @@ -94,7 +94,13 @@ char buf[255]; #define MY_BITMAP_PART rb->lcd_mono_bitmap_part #endif -#if LCD_WIDTH > LCD_HEIGHT +#if (LCD_WIDTH == 400) && (LCD_HEIGHT == 240) +/* boxes as wide as the screen allows make a board taller than it: take + * the 320x240 ones, which its board items are drawn for */ +#define BOX_WIDTH ((320-(MARGIN*2))/BOARD_SIZE) +#define BOX_HEIGHT ((BOX_WIDTH*2)/3) + +#elif LCD_WIDTH > LCD_HEIGHT #define BOX_WIDTH ((LCD_WIDTH-(MARGIN*2))/BOARD_SIZE) #define BOX_HEIGHT ((BOX_WIDTH*2)/3) diff --git a/apps/plugins/wormlet.c b/apps/plugins/wormlet.c index 065769e7ea..e5ccc26958 100644 --- a/apps/plugins/wormlet.c +++ b/apps/plugins/wormlet.c @@ -523,7 +523,7 @@ CONFIG_KEYPAD == MROBE500_PAD #define SPEED 4 #define MAX_WORM_SEGMENTS 512 #elif ((LCD_WIDTH == 240) && (LCD_HEIGHT == 240)) || \ - ((LCD_WIDTH == 320) && (LCD_HEIGHT == 240)) || \ + (((LCD_WIDTH == 320) || (LCD_WIDTH == 400)) && (LCD_HEIGHT == 240)) || \ ((LCD_WIDTH == 240) && ((LCD_HEIGHT == 320) || (LCD_HEIGHT == 400))) || \ ((LCD_WIDTH == 360) && ((LCD_HEIGHT == 400) || (LCD_HEIGHT == 640))) #define FOOD_SIZE 7 From 6350d3f7a0d0f3e7f719e2c9857d64b5f151ae98 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 2 Oct 2026 00:24:12 +0200 Subject: [PATCH 327/408] YP-CP3: build plugins The YP-CP3 shares the YP-R0's keypad and so its plugin keymaps; with the 400x240 screen handled every plugin builds. Co-Authored-By: Claude Opus 5.5 Change-Id: I516ab057c081e710cbc1cf6eca2b24ee87231244 --- tools/configure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/configure b/tools/configure index 88ddda707f..47c620dc7e 100755 --- a/tools/configure +++ b/tools/configure @@ -3050,7 +3050,7 @@ fi output="rockbox.rkw" bootoutput="bootloader.rkw" appextra="" - plugins="" + plugins="yes" # toolset is the tools within the tools directory that we build for # this particular target. toolset="$genericbitmaptools" From 152c3485374d8f56c2c1bef4b6565522a00db676 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Thu, 1 Oct 2026 21:14:38 -0400 Subject: [PATCH 328/408] FS#14025 Checkwps now validates theme cfg files This allows fonts, backdrops, and wps/fms/sbs to be checked. Note that while settings _names_ are validated, the _values_ can not always be checked. Detectable settings errors are flagged, but are not considered fatal. Change-Id: I2a2b7ad94462e983345a1e692eccd6bd57e90eb9 --- apps/misc.c | 4 ++ apps/settings.c | 7 ++ tools/checkwps/checkwps.c | 130 +++++++++++++++++++++++++++++++++++--- 3 files changed, 132 insertions(+), 9 deletions(-) diff --git a/apps/misc.c b/apps/misc.c index 7aa931636d..af9397ad2a 100644 --- a/apps/misc.c +++ b/apps/misc.c @@ -297,6 +297,8 @@ int fast_readline(int fd, char *buf, int buf_size, void *parameters, return 0; } +#endif /* !defined(__PCTOOL__) */ + /* parse a line from a configuration file. the line format is: name: value @@ -338,6 +340,8 @@ bool settings_parseline(char* line, char** name, char** value) return true; } +#ifndef __PCTOOL__ + static void system_flush(void) { playlist_shutdown(); diff --git a/apps/settings.c b/apps/settings.c index f9b170a52d..93e278161c 100644 --- a/apps/settings.c +++ b/apps/settings.c @@ -148,6 +148,8 @@ static void rename_temp_file(const char *tempfile, } #endif /* ndef ROCKBOX_NO_TEMP_SETTINGS_FILE */ +#endif // !__PCTOOL__ + const char* setting_get_cfgvals(const struct settings_list *setting) { if ((setting->flags & F_TABLE_SETTING) == F_TABLE_SETTING) @@ -164,6 +166,8 @@ const char* setting_get_cfgvals(const struct settings_list *setting) return NULL; } +#ifndef __PCTOOL__ + /* calculates and stores crc of settings, returns true if settings have changed */ static bool settings_crc_changed(void) { @@ -211,6 +215,8 @@ void settings_load(void) settings_crc_changed(); } +#endif // !__PCTOOL__ + bool cfg_string_to_int(const struct settings_list *setting, int* out, const char* str) { const char* ptr = setting_get_cfgvals(setting); @@ -396,6 +402,7 @@ bool string_to_cfg(const char *name, char* value, bool *theme_changed) return true; } +#ifndef __PCTOOL__ bool settings_load_config(const char* file, bool apply) { logf("%s()\r\n", __func__); diff --git a/tools/checkwps/checkwps.c b/tools/checkwps/checkwps.c index 9b14c7c747..0d81459907 100644 --- a/tools/checkwps/checkwps.c +++ b/tools/checkwps/checkwps.c @@ -33,6 +33,7 @@ #include "viewport.h" #include "file.h" #include "font.h" +#include "misc.h" bool debug_wps = false; int wps_verbose_level = 0; @@ -229,29 +230,39 @@ bool radio_hardware_present(void) static int loaded_fonts = 0; static struct font _font; -int font_load(const char *path) +int checkwps_loader(const char *path, int mode) { /* First see if it exists in the theme */ int fd = open(path, O_RDONLY); + if (fd < 0) { char buf[1024]; - sprintf(buf, ".rockbox/%s", path); + if (mode) + sprintf(buf, "%s", path); + else + sprintf(buf, ".rockbox/%s", path); fd = open(buf, O_RDONLY); if (fd < 0) { char *first = strrchr(buf, '/'); char *final = strrchr(buf, '.'); - *final = 0; + if (final) *final = 0; int missing = 1; + /* Check if font is included in the bundle */ for (int i = 0 ; bundledfonts[i] != NULL ; i++) { - if (!strcmp(first+1, bundledfonts[i])) { + if (first && !strcmp(first+1, bundledfonts[i])) { missing = 0; break; } } if (missing) { - //printf("Font missing >%s<\n", first+1); - return -1; + if (mode == 2) { + printf("ERROR: File missing: %s\n", path); + return -2; + } else { + printf("Font missing >%s<\n", first+1); + return -1; + } } else { printf("INFO: Theme requires rockbox font bundle\n"); } @@ -260,11 +271,17 @@ int font_load(const char *path) if (fd >= 0) close(fd); + if (mode) + return 0; + int id = 2 + loaded_fonts; loaded_fonts++; return id; } - +int font_load(const char *path) +{ + return checkwps_loader(path, 0); +} void font_unload(int font_id) { (void)font_id; @@ -275,6 +292,85 @@ struct font* font_get(int font) return &_font; } +bool settings_load_config(const char* file, bool apply) +{ + int fd; + char line[128]; + bool theme_changed = false; + fd = open_utf8(file, O_RDONLY); + if (fd < 0) + return false; + + while (read_line(fd, line, sizeof line) > 0) + { + char *name, *value; + if (!settings_parseline(line, &name, &value)) + continue; + + if (!strcmp(name, "font")) { + if (checkwps_loader(value + 1, 1)) + return false; + } + else +#if LCD_DEPTH > 1 || LCD_REMOTE_DEPTH > 1 + if (!strcmp(name, "backdrop")) + { + if (checkwps_loader(value + 1, 2)) + return false; + } +#endif + else if (!strcmp(name, "wps")) + { + if (value[0] != '-') + if (checkwps_loader(value + 1, 2)) + return false; + } + else if (!strcmp(name, "sbs")) + { + if (value[0] != '-') + if (checkwps_loader(value + 1, 2)) + return false; + } + else if (!strcmp(name, "fms")) + { + if (value[0] != '-') + if (checkwps_loader(value + 1, 2)) + return false; + } + else if (!strcmp(name, "rwps")) + { + if (value[0] != '-') + if (checkwps_loader(value + 1, 2)) + return false; + } + else if (!strcmp(name, "rsbs")) + { + if (value[0] != '-') + if (checkwps_loader(value + 1, 2)) + return false; + } + else if (!strcmp(name, "rfms")) + { + if (value[0] != '-') + if (checkwps_loader(value + 1, 2)) + return false; + } + if (!string_to_cfg(name, value, &theme_changed)) + { + if (strcmp(name, "fms") && strcmp(name, "rfms")) { + /* Unknown settings are ignored by firmware */ + printf("WARNING: Invalid setting: %s = %s\n", name, value); + continue; + } + } + } /* while(...) */ + + close(fd); + + (void)apply; + return true; +} + /* This is no longer defined in ROCKBOX builds so just use a huge value */ #define SKIN_BUFFER_SIZE (200*1024) @@ -327,6 +423,10 @@ int check_filetype(const char *ext, enum skinnable_screens *skin, return 1; #endif } + else if (!strcmp(ext, "cfg")) + { + return 2; + } else return -1; @@ -349,7 +449,8 @@ int main(int argc, char **argv) strcmp(argv[1],"-h") == 0 || strcmp(argv[1],"--help") == 0 ) { - printf("Usage: checkwps [OPTIONS] filename.wps [filename2.sbs]...\n"); + printf("Usage: checkwps [OPTIONS] [filename1] [filename2] ...\n"); + printf(" supported file types: wps, sbs, fms, cfg, rwps, rsbs, rfms\n"); printf("\nOPTIONS:\n"); printf("\t-v\t\tverbose\n"); printf("\t-vv\t\tmore verbose\n"); @@ -398,6 +499,17 @@ int main(int argc, char **argv) ret = 2; goto done; } + else if (valid == 2) + { + if (!settings_load_config(name, false)) + { + printf("Bad configuration\n"); + ret = 3; + goto done; + } + printf("%s parsed OK\n\n", ext); + continue; + } else if (valid > 0) continue; /* skip (unsupported by this target but not an error) */ @@ -406,7 +518,7 @@ int main(int argc, char **argv) if (!res) { printf("%s parsing failure\n", ext); skin_error_format_message(); - ret = 3; + ret = 4; goto done; } From 5dc5c4a06ceb74ae75be49fd60a8a1beceb09f36 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 2 Oct 2026 08:04:24 -0400 Subject: [PATCH 329/408] checkwps: Fix red on monchrome devices Change-Id: Ib9064fea51eaf9ce4a2095c1f1d599fc193d2a6d --- tools/checkwps/checkwps.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/checkwps/checkwps.c b/tools/checkwps/checkwps.c index 0d81459907..934c2497ee 100644 --- a/tools/checkwps/checkwps.c +++ b/tools/checkwps/checkwps.c @@ -318,8 +318,9 @@ bool settings_load_config(const char* file, bool apply) if (checkwps_loader(value + 1, 2)) return false; } + else #endif - else if (!strcmp(name, "wps")) + if (!strcmp(name, "wps")) { if (value[0] != '-') if (checkwps_loader(value + 1, 2)) From 142a6fbb397ebf36a01c9dda453469b55dbbb77f Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 29 Sep 2026 11:55:36 -0400 Subject: [PATCH 330/408] jpeg: decode RGB images Both decoders treated every 3-component image as YCbCr, so RGB JPEGs (as written by cjpeg -rgb, and by some Adobe software) came out with wrong colours. Decide the colour space as libjpeg does: a JFIF marker means YCbCr; otherwise the transform flag of an Adobe APP14 marker decides (0 is RGB); otherwise component IDs 'R', 'G', 'B' mean RGB. Core loader: on colour targets R, G and B are stored in place in the row buffer and the YUV conversion is skipped. Greyscale builds now also decode G and B for RGB and combine them into luma per block, which needs every component to be one block per MCU; other RGB layouts are rejected there. Plugin: RGB needs one block per MCU for every component, otherwise it is rejected (colour targets fall back to jpegp). Colour builds convert the R, G and B planes to YCbCr in place after decoding, so display and greyscale view modes are unchanged; greyscale builds combine R, G and B into luma per block as the core does. Code size on the e200: core loader +351 bytes, plugin decoder +603 bytes. Co-Authored-By: Claude Opus 5.5 Change-Id: Ib24a0b7690ca4c00b3ac01b2f511309efeac5975 --- apps/plugins/imageviewer/jpeg/jpeg_decoder.c | 69 +++++++++-- apps/plugins/imageviewer/jpeg/jpeg_decoder.h | 3 + apps/recorder/jpeg_common.h | 35 ++++++ apps/recorder/jpeg_load.c | 117 ++++++++++++++----- 4 files changed, 184 insertions(+), 40 deletions(-) diff --git a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c index ee4f3bcf62..98566affe8 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c +++ b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c @@ -674,6 +674,16 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) return (-5); /* Huffman table index out of range */ } } + p_jpeg->rgb = n == 3 && jpeg_is_rgb(p_jpeg->jfif, p_jpeg->adobe, + p_jpeg->frameheader[0].ID, p_jpeg->frameheader[1].ID, + p_jpeg->frameheader[2].ID); + /* RGB needs every component in one block per MCU: the + planes are converted to YCbCr in place (colour) or + combined per block (greyscale) */ + if (p_jpeg->rgb + && (p_jpeg->frameheader[0].horizontal_sampling != 1 + || p_jpeg->frameheader[0].vertical_sampling != 1)) + return -3; /* Unsupported SOF0 subsampling */ p_src += 3; /* skip spectral information */ p_jpeg->p_entropy_data = p_src; p_end = p_src; /* exit while loop */ @@ -713,7 +723,6 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) case 0xDC: /* Define Number of Lines */ case 0xDE: /* Define Hierarchical progression */ case 0xDF: /* Expand Reference Component(s) */ - case 0xE0: /* Application Field 0*/ case 0xE1: /* Application Field 1*/ case 0xE2: /* Application Field 2*/ case 0xE3: /* Application Field 3*/ @@ -727,7 +736,6 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) case 0xEB: /* Application Field 11*/ case 0xEC: /* Application Field 12*/ case 0xED: /* Application Field 13*/ - case 0xEE: /* Application Field 14*/ case 0xEF: /* Application Field 15*/ case 0xFE: /* Comment */ { @@ -737,6 +745,19 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) } break; + case 0xE0: /* Application Field 0 (JFIF) */ + case 0xEE: /* Application Field 14 (Adobe) */ + { + int marker = p_src[-1]; + marker_size = *p_src++ << 8; /* Highbyte */ + marker_size |= *p_src++; /* Lowbyte */ + n = MIN(marker_size - 2, p_end - p_src); + jpeg_app_colorspace(marker, p_src, MIN(n, 12), &p_jpeg->jfif, + &p_jpeg->adobe); + p_src += marker_size-2; /* skip segment */ + } + break; + case 0xF0: /* Reserved for JPEG extensions */ case 0xF1: /* Reserved for JPEG extensions */ case 0xF2: /* Reserved for JPEG extensions */ @@ -1387,6 +1408,20 @@ int jpeg_decode(struct jpeg* p_jpeg, unsigned char* p_pixel[3], pf_progress(y, p_jpeg->y_mbl-1); /* notify about decoding progress */ } /* for y */ + if (p_jpeg->rgb) + { /* the display expects YCbCr: convert the equal sized R, G and B + planes in place (JFIF equations) */ + unsigned char *pr = p_pixel[0], *pg = p_pixel[1], *pb = p_pixel[2]; + unsigned char *end = pr + width * height; + for (; pr < end; pr++, pg++, pb++) + { + int r = *pr, g = *pg, b = *pb; + *pr = (77 * r + 150 * g + 29 * b + 128) >> 8; + *pg = clamp_component((-43 * r - 85 * g + 128 * b + 32896) >> 8); + *pb = clamp_component((128 * r - 107 * g - 21 * b + 32896) >> 8); + } + } + return 0; /* success */ } #else /* !HAVE_LCD_COLOR */ @@ -1411,7 +1446,8 @@ int jpeg_decode(struct jpeg* p_jpeg, unsigned char* p_pixel[1], int downscale, int k_need; /* AC coefficients needed up to here */ int zero_need; /* init the block with this many zeros */ - int last_dc_val = 0; + int last_dc_val[3] = {0, 0, 0}; + unsigned char rgb_tmp[2][64]; /* R and G of an RGB MCU */ int store_offs[4]; /* memory offsets: order of Y11 Y12 Y21 Y22 U V */ int restart = p_jpeg->restart_interval; /* MCUs until restart marker */ @@ -1482,10 +1518,10 @@ int jpeg_decode(struct jpeg* p_jpeg, unsigned char* p_pixel[1], int downscale, /* Section F.2.2.1: decode the DC coefficient difference */ s = huff_decode_dc(&bs, dctbl); - if (ci == 0) /* only for Y component */ + if (ci == 0 || p_jpeg->rgb) /* Y, or all of R, G, B */ { - last_dc_val += s; - block[0] = last_dc_val; /* output it (assumes zag[0] = 0) */ + last_dc_val[ci] += s; + block[0] = last_dc_val[ci]; /* output it (zag[0] = 0) */ /* coefficient buffer must be cleared */ MEMSET(block+1, 0, zero_need*sizeof(block[0])); @@ -1536,18 +1572,35 @@ int jpeg_decode(struct jpeg* p_jpeg, unsigned char* p_pixel[1], int downscale, } } /* for k */ - if (ci == 0) + if (ci == 0 && !p_jpeg->rgb) { /* only for Y component */ pf_idct(p_byte+store_offs[blkn], block, p_jpeg->qt_idct[ci], skip_line); } + else if (p_jpeg->rgb && ci < 2) + { /* keep R and G until B is decoded */ + pf_idct(rgb_tmp[ci], block, p_jpeg->qt_idct[ci], 8); + } + else if (p_jpeg->rgb) + { /* luma from R, G and B (JFIF weights) */ + int n = 8 / downscale; + int xi, yi; + unsigned char *p = p_byte; + pf_idct(p, block, p_jpeg->qt_idct[ci], skip_line); + for (yi = 0; yi < n; yi++, p += skip_line) + for (xi = 0; xi < n; xi++) + p[xi] = (77 * rgb_tmp[0][yi * 8 + xi] + + 150 * rgb_tmp[1][yi * 8 + xi] + + 29 * p[xi] + 128) >> 8; + } } /* for blkn */ p_byte += skip_mcu; if (p_jpeg->restart_interval && --restart == 0) { /* if a restart marker is due: */ restart = p_jpeg->restart_interval; /* count again */ search_restart(&bs); /* align the bitstream */ - last_dc_val = 0; /* reset decoder */ + last_dc_val[0] = last_dc_val[1] = + last_dc_val[2] = 0; /* reset decoder */ } } /* for x */ if (pf_progress != NULL) diff --git a/apps/plugins/imageviewer/jpeg/jpeg_decoder.h b/apps/plugins/imageviewer/jpeg/jpeg_decoder.h index 9262fcf7ff..307b57ff4a 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg_decoder.h +++ b/apps/plugins/imageviewer/jpeg/jpeg_decoder.h @@ -37,6 +37,9 @@ struct jpeg int blocks; /* blocks per MB */ int restart_interval; /* number of MCUs between RSTm markers */ int store_pos[4]; /* for Y block ordering */ + bool jfif; /* saw a JFIF APP0 marker */ + unsigned char adobe; /* Adobe APP14 transform flag + 1, 0 if none */ + bool rgb; /* the components are R, G, B rather than Y, Cb, Cr */ unsigned char* p_entropy_data; unsigned char* p_entropy_end; diff --git a/apps/recorder/jpeg_common.h b/apps/recorder/jpeg_common.h index fb152feb05..eaa439924c 100644 --- a/apps/recorder/jpeg_common.h +++ b/apps/recorder/jpeg_common.h @@ -27,6 +27,7 @@ #ifndef _JPEG_COMMON_H #define _JPEG_COMMON_H +#include #include "bmp.h" #define HUFF_LOOKAHEAD 8 /* # of bits of lookahead */ @@ -80,6 +81,40 @@ union uint8_rgbyuv { struct uint8_rgb rgb; }; +/* Record what an APP0 (JFIF) or APP14 (Adobe) segment says about the + * colour space, given its first n bytes (at most 12 are needed). *adobe gets + * the Adobe transform flag + 1. */ +static inline void jpeg_app_colorspace(int marker, const unsigned char *id, + int n, bool *jfif, unsigned char *adobe) +{ + bool is_jfif = marker == 0xE0; + const char *sig = is_jfif ? "JFIF" : "Adobe"; /* 5 bytes with the NUL */ + int i; + + if (n < (is_jfif ? 5 : 12)) + return; + for (i = 0; i < 5; i++) + if (id[i] != (unsigned char)sig[i]) + return; + if (is_jfif) + *jfif = true; + else + *adobe = id[11] + 1; +} + +/* Whether a 3-component image holds RGB rather than YCbCr, decided as + * libjpeg does: a JFIF marker means YCbCr; otherwise an Adobe marker's + * transform flag decides (0 is RGB); otherwise component IDs 'R', 'G', 'B' + * mean RGB. adobe is the transform flag + 1, or 0 without an Adobe marker. */ +static inline bool jpeg_is_rgb(bool jfif, int adobe, int id0, int id1, int id2) +{ + if (jfif) + return false; + if (adobe) + return adobe == 1; + return id0 == 'R' && id1 == 'G' && id2 == 'B'; +} + static inline int clamp_component(int x) { if(x > 255) return 255; diff --git a/apps/recorder/jpeg_load.c b/apps/recorder/jpeg_load.c index eb10d1e94f..e81aba8ddf 100644 --- a/apps/recorder/jpeg_load.c +++ b/apps/recorder/jpeg_load.c @@ -56,6 +56,13 @@ typedef uint8_t jpeg_pix_t; #endif #define JPEG_IDCT_TRANSPOSE #define JPEG_PIX_SZ (sizeof(jpeg_pix_t)) +/* index into the per-scale arrays: luma or chroma in colour builds; a + greyscale build only decodes chroma for RGB, which is at the luma scale */ +#ifdef HAVE_LCD_COLOR +#define SCALE_IDX(ci) (!!(ci)) +#else +#define SCALE_IDX(ci) 0 +#endif #ifdef HAVE_LCD_COLOR #define COLOR_EXTRA_IDCT_WS 64 #else @@ -106,14 +113,16 @@ struct jpeg int cur_row; /* current row relative to top of image */ int set_rows; int store_pos[4]; /* for Y block ordering */ -#ifdef HAVE_LCD_COLOR + bool jfif; /* saw a JFIF APP0 marker */ + unsigned char adobe; /* Adobe APP14 transform flag + 1, 0 if none */ + bool rgb; /* the components are R, G, B rather than Y, Cb, Cr */ int last_dc_val[3]; +#ifdef HAVE_LCD_COLOR int h_scale[2]; /* horizontal scalefactor = (2**N) / 8 */ int v_scale[2]; /* same as above, for vertical direction */ int k_need[2]; /* per component zig-zag index of last needed coefficient */ int zero_need[2]; /* per compenent number of coefficients to zero */ #else - int last_dc_val; int h_scale[1]; /* horizontal scalefactor = (2**N) / 8 */ int v_scale[1]; /* same as above, for vertical direction */ int k_need[1]; /* per component zig-zag index of last needed coefficient */ @@ -1198,6 +1207,17 @@ static int process_markers(struct jpeg* p_jpeg) return (-5); /* Huffman table index out of range */ } } + p_jpeg->rgb = n == 3 && jpeg_is_rgb(p_jpeg->jfif, p_jpeg->adobe, + p_jpeg->frameheader[0].ID, p_jpeg->frameheader[1].ID, + p_jpeg->frameheader[2].ID); +#ifndef HAVE_LCD_COLOR + /* greyscale RGB is combined per block, so all components + must be one block per MCU (chroma is then 1x1 too) */ + if (p_jpeg->rgb + && (p_jpeg->frameheader[0].horizontal_sampling != 1 + || p_jpeg->frameheader[0].vertical_sampling != 1)) + return -3; /* Unsupported SOF0 subsampling */ +#endif /* skip spectral information */ e_skip_bytes(p_jpeg, marker_size); done = true; @@ -1264,11 +1284,22 @@ static int process_markers(struct jpeg* p_jpeg) case 0xEF: /* Application Field 15*/ case 0xFE: /* Comment */ { + unsigned char id[12]; + int marker = c; marker_size = e_getc(p_jpeg, -1) << 8; /* Highbyte */ marker_size |= e_getc(p_jpeg, -1); /* Lowbyte */ marker_size -= 2; + n = 0; + if (marker == 0xE0 || marker == 0xEE) + { /* JFIF or Adobe: may say which colour space is used */ + n = MIN(marker_size, (int)sizeof(id)); + for (i = 0; i < n; i++) + id[i] = e_getc(p_jpeg, -1); + jpeg_app_colorspace(marker, id, n, &p_jpeg->jfif, + &p_jpeg->adobe); + } JDEBUGF("unhandled marker len %d\n", marker_size); - e_skip_bytes(p_jpeg, marker_size); /* skip segment */ + e_skip_bytes(p_jpeg, marker_size - n); /* skip segment */ } break; @@ -1546,13 +1577,14 @@ INLINE void fix_quant_tables(struct jpeg *p_jpeg) #ifdef HAVE_LCD_COLOR const int nc = p_jpeg->blocks > 1 ? 3 : 1; #else - const int nc = 1; /* chroma is not decoded */ + /* chroma is not decoded; RGB is, at the luma scale */ + const int nc = p_jpeg->rgb ? 3 : 1; #endif for (c = 0; c < nc; c++) { int t = p_jpeg->frameheader[c].quanttable_select; - int shift = idct_tbl[p_jpeg->v_scale[!!c]].scale; + int shift = idct_tbl[p_jpeg->v_scale[SCALE_IDX(c)]].scale; for (f = 0; f < 4; f++) /* a slot already holding t at this scale? */ if (orig[f] == t && want[f] == shift) @@ -1823,6 +1855,9 @@ static struct img_part *store_row_jpeg(void *jpeg_args) store_offs[p_jpeg->store_pos[3]] = store_offs[1] + store_offs[2]; /* decoded DCT coefficients */ int16_t block[IDCT_WS_SIZE] __attribute__((aligned(8))); +#ifndef HAVE_LCD_COLOR + unsigned char rgb_tmp[2][64]; /* R and G of a greyscale RGB MCU */ +#endif for (x = 0; x < p_jpeg->x_mbl; x++) { int blkn; @@ -1830,7 +1865,7 @@ static struct img_part *store_row_jpeg(void *jpeg_args) { int ci = p_jpeg->mcu_membership[blkn]; /* component index */ #ifdef JPEG_IDCT_TRANSPOSE - bool transpose = p_jpeg->v_scale[!!ci] > 2; + bool transpose = p_jpeg->v_scale[SCALE_IDX(ci)] > 2; #endif int k = 1; /* coefficient index */ int s, r; /* huffman values */ @@ -1845,21 +1880,16 @@ static struct img_part *store_row_jpeg(void *jpeg_args) huff_decode_dc(p_jpeg, dctbl, s, r); #ifndef HAVE_LCD_COLOR - if (!ci) + if (!ci || p_jpeg->rgb) #endif { s = HUFF_EXTEND(r, s); -#ifdef HAVE_LCD_COLOR p_jpeg->last_dc_val[ci] += s; /* output it (assumes zag[0] = 0) */ block[0] = MULTIPLY16(p_jpeg->last_dc_val[ci], qt[0]); -#else - p_jpeg->last_dc_val += s; - /* output it (assumes zag[0] = 0) */ - block[0] = MULTIPLY16(p_jpeg->last_dc_val, qt[0]); -#endif /* coefficient buffer must be cleared */ - MEMSET(block+1, 0, p_jpeg->zero_need[!!ci] * sizeof(int)); + MEMSET(block+1, 0, + p_jpeg->zero_need[SCALE_IDX(ci)] * sizeof(int)); /* Section F.2.2.2: decode the AC coefficients */ while(true) { @@ -1870,7 +1900,7 @@ static struct img_part *store_row_jpeg(void *jpeg_args) if (s) { check_bit_buffer(p_jpeg, s); - if (k >= p_jpeg->k_need[!!ci]) + if (k >= p_jpeg->k_need[SCALE_IDX(ci)]) goto skip_rest; r = get_bits(p_jpeg, s); r = HUFF_EXTEND(r, s); @@ -1912,26 +1942,52 @@ skip_rest: } /* for k */ block_end: #ifndef HAVE_LCD_COLOR - if (!ci) + if (!ci || p_jpeg->rgb) #endif { - int idct_cols = BIT_N(MIN(p_jpeg->h_scale[!!ci], 3)); - int idct_rows = BIT_N(p_jpeg->v_scale[!!ci]); - unsigned char *b_out = out + ci + int sc = SCALE_IDX(ci); + int idct_cols = BIT_N(MIN(p_jpeg->h_scale[sc], 3)); + int idct_rows = BIT_N(p_jpeg->v_scale[sc]); + int rowstep = b_width; +#ifdef HAVE_LCD_COLOR + /* Y, Cb, Cr go to bytes 0-2 for the YUV conversion; + R, G, B to their places in struct uint8_rgb */ + unsigned char *b_out = out + (p_jpeg->rgb ? 2 - ci : ci) + store_offs[p_jpeg->mcu_pos[blkn]]; - if (idct_tbl[p_jpeg->v_scale[!!ci]].v_idct) +#else + unsigned char *b_out = out + + store_offs[p_jpeg->mcu_pos[blkn]]; + if (ci < 2 && p_jpeg->rgb) + { /* keep R and G until B is decoded */ + b_out = rgb_tmp[ci]; + rowstep = 8; + } +#endif + if (idct_tbl[p_jpeg->v_scale[sc]].v_idct) #ifdef JPEG_IDCT_TRANSPOSE - idct_tbl[p_jpeg->v_scale[!!ci]].v_idct(block, + idct_tbl[p_jpeg->v_scale[sc]].v_idct(block, transpose ? block + 8 * idct_cols : block + idct_cols); uint16_t * h_block = transpose ? block + 64 : block; - idct_tbl[p_jpeg->h_scale[!!ci]].h_idct(h_block, b_out, - h_block + idct_rows * 8, b_width); + idct_tbl[p_jpeg->h_scale[sc]].h_idct(h_block, b_out, + h_block + idct_rows * 8, rowstep); #else - idct_tbl[p_jpeg->v_scale[!!ci]].v_idct(block, + idct_tbl[p_jpeg->v_scale[sc]].v_idct(block, block + idct_cols); - idct_tbl[p_jpeg->h_scale[!!ci]].h_idct(block, b_out, - block + idct_rows * 8, b_width); + idct_tbl[p_jpeg->h_scale[sc]].h_idct(block, b_out, + block + idct_rows * 8, rowstep); +#endif +#ifndef HAVE_LCD_COLOR + if (ci == 2) + { /* luma from R, G and B (JFIF weights) */ + int w = BIT_N(p_jpeg->h_scale[0]); + int xi, yi; + for (yi = 0; yi < idct_rows; yi++, b_out += b_width) + for (xi = 0; xi < w; xi++) + b_out[xi] = (77 * rgb_tmp[0][yi * 8 + xi] + + 150 * rgb_tmp[1][yi * 8 + xi] + + 29 * b_out[xi] + 128) >> 8; + } #endif } } /* for blkn */ @@ -1958,12 +2014,8 @@ block_end: { /* if a restart marker is due: */ p_jpeg->restart = p_jpeg->restart_interval; /* count again */ search_restart(p_jpeg); /* align the bitstream */ -#ifdef HAVE_LCD_COLOR p_jpeg->last_dc_val[0] = p_jpeg->last_dc_val[1] = p_jpeg->last_dc_val[2] = 0; /* reset decoder */ -#else - p_jpeg->last_dc_val = 0; -#endif } } } /* if !p_jpeg->mcu_row */ @@ -2268,7 +2320,8 @@ int clip_jpeg_fd(int fd, int flags, if (resize) { if (resize_on_load(bm, dither, &src_dim, &rset, buf_start, maxsize, - cformat, IF_PIX_FMT(p_jpeg->blocks == 1 ? 0 : 1,) store_row_jpeg, + cformat, IF_PIX_FMT(p_jpeg->blocks == 1 || p_jpeg->rgb ? 0 : 1,) + store_row_jpeg, p_jpeg)) return bm_size; } else { @@ -2292,7 +2345,7 @@ int clip_jpeg_fd(int fd, int flags, { part = store_row_jpeg(p_jpeg); #ifdef HAVE_LCD_COLOR - if (p_jpeg->blocks > 1) + if (p_jpeg->blocks > 1 && !p_jpeg->rgb) { struct uint8_rgb *qp = part->buf; struct uint8_rgb *end = qp + bm->width; From 9936e65e6dba6480f7cb21d97e569f36f5113f78 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 29 Sep 2026 20:37:25 -0400 Subject: [PATCH 331/408] imageviewer/jpegp: decode RGB images jpegp converted every image from YCbCr, so RGB JPEGs showed scrambled colours. That affects progressive RGB files, and now also baseline RGB files the jpeg decoder rejects and hands on to jpegp, such as RGB with the R component sampled 2x2. Record the JFIF and Adobe APP14 markers, decide the colour space with the same rule as the other decoders, and skip the YUV conversion for RGB. Co-Authored-By: Claude Opus 5.5 Change-Id: If023eeb612b7f8a891d21fbe0070ab43c5a08f17 --- apps/plugins/imageviewer/jpegp/jpeg81.c | 12 ++++++++++-- apps/plugins/imageviewer/jpegp/jpeg81.h | 4 ++++ apps/plugins/imageviewer/jpegp/jpegp.c | 6 ++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/plugins/imageviewer/jpegp/jpeg81.c b/apps/plugins/imageviewer/jpegp/jpeg81.c index 83f4e287fa..b4849da6f7 100644 --- a/apps/plugins/imageviewer/jpegp/jpeg81.c +++ b/apps/plugins/imageviewer/jpegp/jpeg81.c @@ -43,6 +43,7 @@ jpeg81.c #include "GETC.h" #include "rb_glue.h" #include "jpeg81.h" +#include "jpeg_common.h" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" @@ -982,8 +983,15 @@ extern enum JPEGENUM JPEGDecode(struct JPEGD *j) } else if ( (marker & 0xf0) == 0xE0 ) // APPn E0..EF { - int La= GETWbi(); - SEEK(La-2); + int La= GETWbi()-2; + int n= 0; + if ( marker == 0xE0 || marker == 0xEE ) // JFIF, Adobe: colour space + { + unsigned char id[12]; + for ( ; n < La && n < 12; n++ ) id[n]= GETC(); + jpeg_app_colorspace(marker, id, n, &j->jfif, &j->adobe); + } + SEEK(La-n); printf("APP%d\n", marker&15); } else if ( marker == 0xFE ) // COM diff --git a/apps/plugins/imageviewer/jpegp/jpeg81.h b/apps/plugins/imageviewer/jpegp/jpeg81.h index e265d84926..8d9c2a04a3 100644 --- a/apps/plugins/imageviewer/jpegp/jpeg81.h +++ b/apps/plugins/imageviewer/jpegp/jpeg81.h @@ -34,6 +34,8 @@ enum JPEGENUM { JPEGENUMERR_COMPNOTFOUND, // Scan component selector (Csj) not found among Component identifiers (Ci) }; +#include + typedef short TCOEF; // 16-bit coefficients typedef TCOEF DU[64]; // The DATA UNIT typedef unsigned short TSAMP; // Lossless 'coefficients' are unsigned @@ -103,6 +105,8 @@ struct JPEGD { // The JPEG DECODER OBJECT void *jpeg_mem; // <-- free me int Hmax, Vmax; // for conversion + bool jfif; // saw a JFIF APP0 marker + unsigned char adobe; // Adobe APP14 transform flag + 1, 0 if none int mcu_width; int mcu_height; int mcu_total; // covers the whole image diff --git a/apps/plugins/imageviewer/jpegp/jpegp.c b/apps/plugins/imageviewer/jpegp/jpegp.c index a5883e3a28..955399b2f6 100644 --- a/apps/plugins/imageviewer/jpegp/jpegp.c +++ b/apps/plugins/imageviewer/jpegp/jpegp.c @@ -4,6 +4,7 @@ #include "rb_glue.h" #include "../imageviewer.h" +#include "jpeg_common.h" /**************** begin Application ********************/ @@ -217,6 +218,8 @@ static int get_image(struct image_info *info, int frame, int ds) int v1 = j->Vmax / j->Components[1].Vi; int h2 = j->Hmax / j->Components[2].Hi; int v2 = j->Vmax / j->Components[2].Vi; + bool rgb = jpeg_is_rgb(j->jfif, j->adobe, j->Components[0].Ci, + j->Components[1].Ci, j->Components[2].Ci); int x, y; int max_y = info->height * ds; @@ -264,6 +267,9 @@ static int get_image(struct image_info *info, int frame, int ds) c2 = (sumV + area/2) / area; } + if (rgb) // components are R, G, B: no conversion + *bmp++= FB_RGBPACK(c0, c1, c2); + else // ITU BT.601 full-range YUV-to-RGB integer approximation { int y = (c0 << 5) + 16; From 265cf7ac92120e9b28aff116b058236912739fd8 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 2 Oct 2026 08:28:55 -0400 Subject: [PATCH 332/408] FS#14026 - Spanish Translation Update (Jordan Fajardo) Change-Id: Idccd1d1a48c1c6f28d77782cc73e36b6868faff3 --- apps/lang/espanol.lang | 228 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 201 insertions(+), 27 deletions(-) diff --git a/apps/lang/espanol.lang b/apps/lang/espanol.lang index 6dffa08c33..b2309015ac 100644 --- a/apps/lang/espanol.lang +++ b/apps/lang/espanol.lang @@ -6925,13 +6925,13 @@ desc: in the equalizer settings menu user: core - *: "Peak Filter %d" + *: "Peak Filter" - *: "Filtro de pico %d" + *: "Filtro de Pico" - *: "Filtro de pico" + *: "Filtro de Pico" @@ -7392,15 +7392,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Frecuencia de corte de bajos" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Corte de Graves" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Frecuencia de corte de bajos" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Corte de Graves" @@ -8934,15 +8934,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Frec. de corte de agudos" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Corte de Agudos" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Frecuencia de corte de agudos" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Corte de Agudos" @@ -15018,20 +15018,6 @@ *: "No es un plugin" - - id: LANG_OPEN_PLUGIN_SET_WPS_CONTEXT_PLUGIN - desc: open plugin module - user: core - - *: "Set WPS Context Plugin" - - - *: "Establecer Plugin de contexto WPS" - - - *: "Establecer Plugin de contexto WPS" - - id: LANG_PARAMETER desc: @@ -15881,16 +15867,16 @@ id: LANG_DEFAULT_BROWSER - desc: in Settings + desc: deprecated user: core - *: "Default Browser" + *: "" - *: "Navegador predeterminado" + *: "" - *: "Navegador predeterminado" + *: "" @@ -16995,3 +16981,191 @@ *: "~U S B" + + id: LANG_SET_CONTEXT_ITEM + desc: WPS context item + user: core + + *: "Set Context Item" + + + *: "Definir Elemento de Contexto" + + + *: "Definir Elemento de Contexto" + + + + id: LANG_FILE_NOT_FOUND + desc: When file does not exist + user: core + + *: "File not found" + + + *: "Archivo no Encontrado" + + + *: "Archivo no Encontrado" + + + + id: LANG_SHOW_IN_FILES + desc: Reveal item in File Browser + user: core + + *: "Show in Files" + + + *: "Mostrar en Archivos" + + + *: "Mostrar en Archivos" + + + + id: LANG_CHANNEL_SWAP + desc: in sound_settings + user: core + + *: "Swap Left & Right" + + + *: "Intercambiar Izquierda & Derecha" + + + *: "Intercambiar Izquierda & Derecha" + + + + id: LANG_COUNTDOWN_TIMER_SET + desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration + user: core + + *: "SET TIMER" + + + *: "Establecer Temporizador" + + + *: "Establecer Temporizador" + + + + id: LANG_COUNTDOWN_TIMER_RUNNING + desc: countdown_timer plugin - status label shown while the countdown is active + user: core + + *: "RUNNING" + + + *: "En Marcha" + + + *: "En Marcha" + + + + id: LANG_COUNTDOWN_TIMER_PAUSED + desc: countdown_timer plugin - status label shown while the countdown is paused + user: core + + *: "PAUSED" + + + *: "En Pausa" + + + *: "En Pausa" + + + + id: LANG_COUNTDOWN_TIMER_OVERTIME + desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up + user: core + + *: "OVERTIME" + + + *: "Extratiempo" + + + *: "Extratiempo" + + + + id: LANG_COUNTDOWN_TIMER_FINISHED + desc: countdown_timer plugin - status label shown at the moment the countdown expires + user: core + + *: "FINISHED" + + + *: "Finalizado" + + + *: "Finalizado" + + + + id: LANG_EQUALIZER_BAND + desc: in the equalizer settings menu + user: core + + *: "Filter %d" + + + *: "Filter %d" + + + *: "Filter" + + + + id: LANG_EQUALIZER_FILTER_TYPE + desc: in the equalizer settings menu + user: core + + *: "Filter Type" + + + *: "Tipo de Filtro" + + + *: "Tipo de Filtro" + + + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "Salida de Video Compuesto" + + + *: none + videoout: "Salida de Video Compuesto" + + + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "Atajos del Flick Tactil" + + + *: none + touchscreen: "Atajos del Flick Tactil" + + From c0ae6c7cd1b0d6ae59d2e99cd684b7e0962037f5 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 29 Sep 2026 20:22:45 -0400 Subject: [PATCH 333/408] FS#14019: Detect and reject outdated binary language files (WIP) Accomplish this by checksumming the english language input and (1) including that in binary files and (2) checking the value matches what was compiled into the firmware image Not sure if this is the best approach but it works. Change-Id: I8f79ad1b9d1cdf69e6a085d7b3dd1b5e078af04b --- apps/lang/lang.make | 6 +++--- apps/language.c | 9 +++++---- tools/genlang | 24 ++++++++++++++++++++---- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/apps/lang/lang.make b/apps/lang/lang.make index 5923597e86..405444e022 100644 --- a/apps/lang/lang.make +++ b/apps/lang/lang.make @@ -20,18 +20,18 @@ CLEANOBJS += $(BUILDDIR)/lang/max_language_size.h $(BUILDDIR)/lang/lang* #DUMMY := $(shell mkdir -p $(BUILDDIR)/apps/lang) # Calculate the maximum language size. Currently based on the file size -# of the largest lng file. Subtract 10 due to HEADER_SIZE and +# of the largest lng file. Subtract 11 due to HEADER_SIZE and # SUBHEADER_SIZE. # TODO: In the future generate this file within genlang or another script # in order to only calculate the maximum size based on the core strings. $(BUILDDIR)/lang/max_language_size.h: $(LANGOBJ) $(BUILDDIR)/apps/lang/voicestrings.zip $(call PRINTS,GEN $(subst $(BUILDDIR)/,,$@)) - $(SILENT)echo "#define MAX_LANGUAGE_SIZE `ls -ln $(BUILDDIR)/apps/lang/*.lng | awk '{print $$5-10}' | sort -n | tail -1`" > $@ + $(SILENT)echo "#define MAX_LANGUAGE_SIZE `ls -ln $(BUILDDIR)/apps/lang/*.lng | awk '{print $$5-11}' | sort -n | tail -1`" > $@ $(BUILDDIR)/lang/lang_core.o: $(BUILDDIR)/lang/lang.h $(BUILDDIR)/lang/lang_core.c $(call PRINTS,CC lang_core.c)$(CC) $(CFLAGS) -c $(BUILDDIR)/lang/lang_core.c -o $@ -# genlang creates *both* lang.c and lang.h but in Make there is no wat to express this rule +# genlang creates *both* lang.c and lang.h but in Make there is no way to express this rule # (multiple target rules DO NOT express that, they are a simple shortcut for multiple rules) # instead we pretend that genlang create lang_core.c and that lang.c depends from lang.h # it will work fine as long as one never manually removes lang.c and not lang.h, and it will avoid diff --git a/apps/language.c b/apps/language.c index 096c708e62..d63aea6021 100644 --- a/apps/language.c +++ b/apps/language.c @@ -35,12 +35,12 @@ /* These defines must match the initial bytes in the binary lang file */ /* See tools/genlang (TODO: Use common include for both) */ #define LANGUAGE_COOKIE 0x1a -#define LANGUAGE_VERSION 0x06 +#define LANGUAGE_VERSION 0x07 #define LANGUAGE_FLAG_RTL 0x01 #define LANGUAGE_FLAG_UNITS_FIRST 0x02 -#define HEADER_SIZE 4 +#define HEADER_SIZE 5 #define SUBHEADER_SIZE 6 static unsigned char language_buffer[MAX_LANGUAGE_SIZE]; @@ -72,8 +72,9 @@ int lang_load(const char *filename, const unsigned char *builtin, if(read(fd, lang_header, HEADER_SIZE) == HEADER_SIZE && ((lang_header[0] == LANGUAGE_COOKIE) && - (lang_header[1] == LANGUAGE_VERSION) && - (lang_header[2] == TARGET_ID))) { + (lang_header[1] == LANGUAGE_VERSION) && + (lang_header[2] == TARGET_ID) && + (lang_header[4] == ENGLISH_LANG_CHECKSUM)) ) { /* jump to the proper entry in the table of subheaders */ lseek(fd, user_num * SUBHEADER_SIZE, SEEK_CUR); if (read(fd, sub_header, SUBHEADER_SIZE) != SUBHEADER_SIZE) diff --git a/tools/genlang b/tools/genlang index 3cd470d0a1..c6b7645677 100755 --- a/tools/genlang +++ b/tools/genlang @@ -9,11 +9,14 @@ # Copyright (C) 2006 - 2008 by Daniel Stenberg # +use Digest::SHA1; + # See apps/language.c (TODO: Use common include for both) # Cookie and binary version for the binary lang file my $LANGUAGE_COOKIE = 0x1a; +my $LANGUAGE_VERSION = 0x07; my $VOICE_COOKIE = 0x9a; -my $LANGUAGE_VERSION = 0x06; +my $VOICE_VERSION = 0x06; my $LANGUAGE_FLAG_RTL = 0x01; my $LANGUAGE_FLAG_UNITS_FIRST = 0x02; @@ -70,6 +73,16 @@ if (!$english) { print STDERR "Please specify the english lang source (with -e)!\n"; exit; } +my $english_crc; +{ + open my $fh, "<$english" or die $!; + binmode $fh; + my $sha = Digest::SHA1->new; + $sha->addfile($fh); + $english_crc = $sha->hexdigest; + $english_crc = substr($english_crc, 0, 2); + close $fh; +} my $target_id = $i; if($binary && !$target_id) { @@ -501,6 +514,9 @@ extern unsigned char *language_strings[]; /* this contains the concatenation of all strings, separated by \\0 chars */ extern const unsigned char core_language_builtin[]; +/* this is a checksum of the english language file */ +#define ENGLISH_LANG_CHECKSUM 0x$english_crc + #include "${prefix}_enum.h" MOO @@ -604,13 +620,13 @@ elsif($binary || $binvoice) { if($binary) { open(OUTF, ">$binary") or die "Error: Can't create $binary"; binmode OUTF; - printf OUTF ("%c%c%c%c", $LANGUAGE_COOKIE, $LANGUAGE_VERSION, $target_id, - $langoptions); # magic lang file header + printf OUTF ("%c%c%c%c%c", $LANGUAGE_COOKIE, $LANGUAGE_VERSION, $target_id, + $langoptions, hex($english_crc)); # magic lang file header } if($binvoice) { open(OUTV, ">$binvoice") or die "Error: Can't create $binary"; binmode OUTV; - printf OUTV ("%c%c%c%c", $VOICE_COOKIE, $LANGUAGE_VERSION, $target_id, + printf OUTV ("%c%c%c%c", $VOICE_COOKIE, $VOICE_VERSION, $target_id, $langoptions); # magic lang file header } From 025284d95ad7ddea64d2b37bbc115b457b8d82ba Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 2 Oct 2026 09:59:46 -0400 Subject: [PATCH 334/408] checkwps: '-' is also valid for backdrops Change-Id: I67c2d871f053afca277c8fc4ad671f9689f5c647 --- tools/checkwps/checkwps.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/checkwps/checkwps.c b/tools/checkwps/checkwps.c index 934c2497ee..bdee0c262f 100644 --- a/tools/checkwps/checkwps.c +++ b/tools/checkwps/checkwps.c @@ -315,8 +315,9 @@ bool settings_load_config(const char* file, bool apply) #if LCD_DEPTH > 1 || LCD_REMOTE_DEPTH > 1 if (!strcmp(name, "backdrop")) { - if (checkwps_loader(value + 1, 2)) - return false; + if (value[0] != '-') + if (checkwps_loader(value + 1, 2)) + return false; } else #endif From 9910b2aebfb53700d6271483f14133d71d4b6603 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 2 Oct 2026 10:18:29 -0400 Subject: [PATCH 335/408] Revert "FS#14019: Detect and reject outdated binary language files (WIP)" This reverts commit c0ae6c7cd1b0d6ae59d2e99cd684b7e0962037f5. --- apps/lang/lang.make | 6 +++--- apps/language.c | 9 ++++----- tools/genlang | 24 ++++-------------------- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/apps/lang/lang.make b/apps/lang/lang.make index 405444e022..5923597e86 100644 --- a/apps/lang/lang.make +++ b/apps/lang/lang.make @@ -20,18 +20,18 @@ CLEANOBJS += $(BUILDDIR)/lang/max_language_size.h $(BUILDDIR)/lang/lang* #DUMMY := $(shell mkdir -p $(BUILDDIR)/apps/lang) # Calculate the maximum language size. Currently based on the file size -# of the largest lng file. Subtract 11 due to HEADER_SIZE and +# of the largest lng file. Subtract 10 due to HEADER_SIZE and # SUBHEADER_SIZE. # TODO: In the future generate this file within genlang or another script # in order to only calculate the maximum size based on the core strings. $(BUILDDIR)/lang/max_language_size.h: $(LANGOBJ) $(BUILDDIR)/apps/lang/voicestrings.zip $(call PRINTS,GEN $(subst $(BUILDDIR)/,,$@)) - $(SILENT)echo "#define MAX_LANGUAGE_SIZE `ls -ln $(BUILDDIR)/apps/lang/*.lng | awk '{print $$5-11}' | sort -n | tail -1`" > $@ + $(SILENT)echo "#define MAX_LANGUAGE_SIZE `ls -ln $(BUILDDIR)/apps/lang/*.lng | awk '{print $$5-10}' | sort -n | tail -1`" > $@ $(BUILDDIR)/lang/lang_core.o: $(BUILDDIR)/lang/lang.h $(BUILDDIR)/lang/lang_core.c $(call PRINTS,CC lang_core.c)$(CC) $(CFLAGS) -c $(BUILDDIR)/lang/lang_core.c -o $@ -# genlang creates *both* lang.c and lang.h but in Make there is no way to express this rule +# genlang creates *both* lang.c and lang.h but in Make there is no wat to express this rule # (multiple target rules DO NOT express that, they are a simple shortcut for multiple rules) # instead we pretend that genlang create lang_core.c and that lang.c depends from lang.h # it will work fine as long as one never manually removes lang.c and not lang.h, and it will avoid diff --git a/apps/language.c b/apps/language.c index d63aea6021..096c708e62 100644 --- a/apps/language.c +++ b/apps/language.c @@ -35,12 +35,12 @@ /* These defines must match the initial bytes in the binary lang file */ /* See tools/genlang (TODO: Use common include for both) */ #define LANGUAGE_COOKIE 0x1a -#define LANGUAGE_VERSION 0x07 +#define LANGUAGE_VERSION 0x06 #define LANGUAGE_FLAG_RTL 0x01 #define LANGUAGE_FLAG_UNITS_FIRST 0x02 -#define HEADER_SIZE 5 +#define HEADER_SIZE 4 #define SUBHEADER_SIZE 6 static unsigned char language_buffer[MAX_LANGUAGE_SIZE]; @@ -72,9 +72,8 @@ int lang_load(const char *filename, const unsigned char *builtin, if(read(fd, lang_header, HEADER_SIZE) == HEADER_SIZE && ((lang_header[0] == LANGUAGE_COOKIE) && - (lang_header[1] == LANGUAGE_VERSION) && - (lang_header[2] == TARGET_ID) && - (lang_header[4] == ENGLISH_LANG_CHECKSUM)) ) { + (lang_header[1] == LANGUAGE_VERSION) && + (lang_header[2] == TARGET_ID))) { /* jump to the proper entry in the table of subheaders */ lseek(fd, user_num * SUBHEADER_SIZE, SEEK_CUR); if (read(fd, sub_header, SUBHEADER_SIZE) != SUBHEADER_SIZE) diff --git a/tools/genlang b/tools/genlang index c6b7645677..3cd470d0a1 100755 --- a/tools/genlang +++ b/tools/genlang @@ -9,14 +9,11 @@ # Copyright (C) 2006 - 2008 by Daniel Stenberg # -use Digest::SHA1; - # See apps/language.c (TODO: Use common include for both) # Cookie and binary version for the binary lang file my $LANGUAGE_COOKIE = 0x1a; -my $LANGUAGE_VERSION = 0x07; my $VOICE_COOKIE = 0x9a; -my $VOICE_VERSION = 0x06; +my $LANGUAGE_VERSION = 0x06; my $LANGUAGE_FLAG_RTL = 0x01; my $LANGUAGE_FLAG_UNITS_FIRST = 0x02; @@ -73,16 +70,6 @@ if (!$english) { print STDERR "Please specify the english lang source (with -e)!\n"; exit; } -my $english_crc; -{ - open my $fh, "<$english" or die $!; - binmode $fh; - my $sha = Digest::SHA1->new; - $sha->addfile($fh); - $english_crc = $sha->hexdigest; - $english_crc = substr($english_crc, 0, 2); - close $fh; -} my $target_id = $i; if($binary && !$target_id) { @@ -514,9 +501,6 @@ extern unsigned char *language_strings[]; /* this contains the concatenation of all strings, separated by \\0 chars */ extern const unsigned char core_language_builtin[]; -/* this is a checksum of the english language file */ -#define ENGLISH_LANG_CHECKSUM 0x$english_crc - #include "${prefix}_enum.h" MOO @@ -620,13 +604,13 @@ elsif($binary || $binvoice) { if($binary) { open(OUTF, ">$binary") or die "Error: Can't create $binary"; binmode OUTF; - printf OUTF ("%c%c%c%c%c", $LANGUAGE_COOKIE, $LANGUAGE_VERSION, $target_id, - $langoptions, hex($english_crc)); # magic lang file header + printf OUTF ("%c%c%c%c", $LANGUAGE_COOKIE, $LANGUAGE_VERSION, $target_id, + $langoptions); # magic lang file header } if($binvoice) { open(OUTV, ">$binvoice") or die "Error: Can't create $binary"; binmode OUTV; - printf OUTV ("%c%c%c%c", $VOICE_COOKIE, $VOICE_VERSION, $target_id, + printf OUTV ("%c%c%c%c", $VOICE_COOKIE, $LANGUAGE_VERSION, $target_id, $langoptions); # magic lang file header } From f61d379824bbec737a059f5d25e18076cf527b4c Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 2 Oct 2026 16:21:50 +0200 Subject: [PATCH 336/408] YP-CP3: simulator - uisimulator/bitmaps/UI-samsungypcp3.bmp: the YP-CP3 from the front, 614x324, the 400x240 screen at 40,37. Dithered to RGB565: the simulator converts its background to the 16-bit LCD format, which turned the case's gradients into bands. - sim-ui-defines.h: its window and screen position - buttonmap/samsung-ypcp3.c: the YP-R0's keyboard layout - the YP-CP3 shares its keypad - and click areas for the joystick, Back and Menu below it, and User (Rec) and Power on the top edge above them Co-Authored-By: Claude Opus 5.5 Change-Id: I9146fd54400d85636a97509993b393bff8b1ab78 --- firmware/target/hosted/sdl/sim-ui-defines.h | 7 ++ uisimulator/bitmaps/UI-samsungypcp3.bmp | Bin 0 -> 597510 bytes uisimulator/buttonmap/SOURCES | 4 + uisimulator/buttonmap/samsung-ypcp3.c | 85 ++++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 uisimulator/bitmaps/UI-samsungypcp3.bmp create mode 100644 uisimulator/buttonmap/samsung-ypcp3.c diff --git a/firmware/target/hosted/sdl/sim-ui-defines.h b/firmware/target/hosted/sdl/sim-ui-defines.h index 5a08b27adf..f6686ca619 100644 --- a/firmware/target/hosted/sdl/sim-ui-defines.h +++ b/firmware/target/hosted/sdl/sim-ui-defines.h @@ -390,6 +390,13 @@ #define UI_WIDTH LCD_WIDTH #define UI_HEIGHT LCD_HEIGHT +#elif defined(SAMSUNG_YPCP3) +#define UI_TITLE "Samsung YP-CP3" +#define UI_WIDTH 614 /* width of GUI window */ +#define UI_HEIGHT 324 /* height of GUI window */ +#define UI_LCD_POSX 40 +#define UI_LCD_POSY 37 + #elif defined(SAMSUNG_YPR0) #define UI_TITLE "Samsung YP-R0" #define UI_WIDTH 313 /* width of GUI window */ diff --git a/uisimulator/bitmaps/UI-samsungypcp3.bmp b/uisimulator/bitmaps/UI-samsungypcp3.bmp new file mode 100644 index 0000000000000000000000000000000000000000..caea28a9f6fb749feec388c16dec270fb4787bf0 GIT binary patch literal 597510 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 z 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 literal 0 HcmV?d00001 diff --git a/uisimulator/buttonmap/SOURCES b/uisimulator/buttonmap/SOURCES index b9fa3b64ac..8dc2ccc7d5 100644 --- a/uisimulator/buttonmap/SOURCES +++ b/uisimulator/buttonmap/SOURCES @@ -62,7 +62,11 @@ sansa-connect.c #elif CONFIG_KEYPAD == SONY_NWZ_PAD sony-nwz.c #elif CONFIG_KEYPAD == SAMSUNG_YPR0_PAD +#ifdef SAMSUNG_YPCP3 +samsung-ypcp3.c +#else samsung-ypr0.c +#endif #elif CONFIG_KEYPAD == CREATIVE_ZEN_PAD creative-zen.c #elif CONFIG_KEYPAD == SONY_NWZA860_PAD diff --git a/uisimulator/buttonmap/samsung-ypcp3.c b/uisimulator/buttonmap/samsung-ypcp3.c new file mode 100644 index 0000000000..b678a95722 --- /dev/null +++ b/uisimulator/buttonmap/samsung-ypcp3.c @@ -0,0 +1,85 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 by Marcin Bukat + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + + +#include +#include "button.h" +#include "buttonmap.h" + +/* The YP-CP3 has the YP-R0's keys, and the same keyboard layout here */ +int key_to_button(int keyboard_button) +{ + int new_btn = BUTTON_NONE; + switch (keyboard_button) + { + case SDLK_KP_4: + case SDLK_LEFT: + new_btn = BUTTON_LEFT; + break; + case SDLK_KP_6: + case SDLK_RIGHT: + new_btn = BUTTON_RIGHT; + break; + case SDLK_KP_8: + case SDLK_UP: + new_btn = BUTTON_UP; + break; + case SDLK_KP_2: + case SDLK_DOWN: + new_btn = BUTTON_DOWN; + break; + case SDLK_PAGEUP: + case SDLK_KP_9: + new_btn = BUTTON_MENU; + break; + case SDLK_KP_7: + new_btn = BUTTON_BACK; + break; + case SDLK_KP_5: + case SDLK_SPACE: + case SDLK_KP_ENTER: + case SDLK_RETURN: + new_btn = BUTTON_SELECT; + break; + case SDLK_KP_3: + new_btn = BUTTON_POWER; + break; + case SDLK_KP_1: + new_btn = BUTTON_USER; + break; + } + return new_btn; +} + +/* The face carries the joystick and, below it, Back and Menu; User (Rec) + * and Power sit on the top edge, above them. Select comes first: the first + * area a click falls in wins. */ +struct button_map bm[] = { + { SDLK_KP_5, 517, 90, 11, "Select" }, + { SDLK_KP_8, 517, 66, 13, "Up" }, + { SDLK_KP_2, 517, 114, 13, "Down" }, + { SDLK_KP_4, 493, 90, 13, "Left" }, + { SDLK_KP_6, 541, 90, 13, "Right" }, + { SDLK_KP_7, 519, 159, 11, "Back" }, + { SDLK_KP_9, 519, 193, 11, "Menu" }, + { SDLK_KP_1, 480, 8, 12, "User" }, + { SDLK_KP_3, 531, 8, 12, "Power" }, + { 0, 0, 0, 0, "None" } +}; From 90506b8808025d00d56cf73e5d2a889bb31f7fd8 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 2 Oct 2026 16:40:44 -0400 Subject: [PATCH 337/408] checkwps: Numerous improvements to cfg validation * Check iconset, remote iconset, and viewer iconset * don't crash (or fail) on empty strings * when component path is not absolute, search appropriate path (eg FONT_DIR, ICON_DIR, WPS_DIR, SBS_DIR) Change-Id: I256d00d26d79b1d1f09038877cfaaa804123df72 --- tools/checkwps/checkwps.c | 157 ++++++++++++++++++++++++++------------ 1 file changed, 107 insertions(+), 50 deletions(-) diff --git a/tools/checkwps/checkwps.c b/tools/checkwps/checkwps.c index bdee0c262f..60a72cfe24 100644 --- a/tools/checkwps/checkwps.c +++ b/tools/checkwps/checkwps.c @@ -230,58 +230,97 @@ bool radio_hardware_present(void) static int loaded_fonts = 0; static struct font _font; -int checkwps_loader(const char *path, int mode) + +/* mode 0 is for fonts in the WPS + mode 1 is for fonts in the cfg file + mode 2 is everything else in the cfg file + + search is an optional location to search for relative paths +*/ +static int checkwps_loader(const char *path, const char *search, int mode) { + if (!path) + return -1; + if (!path[0]) + return mode? 0 : -2; /* Empty is ok for non-WPS */ + if (path[0] == '-' && !path[1]) + return mode? 0 : -3; /* Also ok for non-WPS */ + if (path[0] == '/') + path++; /* Absolute path, skip leading / */ + /* First see if it exists in the theme */ int fd = open(path, O_RDONLY); + if (fd >= 0) + goto done; - if (fd < 0) { + { char buf[1024]; - if (mode) - sprintf(buf, "%s", path); - else + if (!mode) { sprintf(buf, ".rockbox/%s", path); - fd = open(buf, O_RDONLY); - if (fd < 0) { + fd = open(buf, O_RDONLY); + if (fd >= 0) + goto done; + } + + if (search) + { + sprintf(buf, ".rockbox/%s/%s", search, path); + fd = open(buf, O_RDONLY); + if (fd >= 0) + goto done; + } + + if (mode == 0 || mode == 1) /* not found and it's a font */ + { char *first = strrchr(buf, '/'); char *final = strrchr(buf, '.'); - if (final) *final = 0; int missing = 1; + if (final) *final = 0; /* Check if font is included in the bundle */ - for (int i = 0 ; bundledfonts[i] != NULL ; i++) { - if (first && !strcmp(first+1, bundledfonts[i])) { + for (int i = 0 ; bundledfonts[i] != NULL ; i++) + { + if (first && !strcmp(first+1, bundledfonts[i])) + { missing = 0; break; } } - if (missing) { - if (mode == 2) { - printf("ERROR: File missing: %s\n", path); - return -2; - } else { - printf("Font missing >%s<\n", first+1); - return -1; - } + + if (missing) + { + printf("Font missing >%s<\n", first+1); + return -4; } else { printf("INFO: Theme requires rockbox font bundle\n"); } } + + if (mode == 2) + { + printf("ERROR: File missing: %s\n", path); + return -5; + } } + +done: if (fd >= 0) close(fd); - if (mode) - return 0; - - int id = 2 + loaded_fonts; - loaded_fonts++; - return id; + return 0; } + int font_load(const char *path) { - return checkwps_loader(path, 0); + int rval = checkwps_loader(path, FONT_DIR, 0); + if (rval) + return rval; + + rval = 2 + loaded_fonts; + loaded_fonts++; + return rval; } + void font_unload(int font_id) { (void)font_id; @@ -306,57 +345,75 @@ bool settings_load_config(const char* file, bool apply) char *name, *value; if (!settings_parseline(line, &name, &value)) continue; - - if (!strcmp(name, "font")) { - if (checkwps_loader(value + 1, 1)) + if (!strcmp(name, "font")) + { + if (checkwps_loader(value, FONT_DIR, 1)) + return false; + } +#ifdef HAVE_REMOTE_LCD + if (!strcmp(name, "remote font")) + { + if (checkwps_loader(value, FONT_DIR, 1)) return false; } else -#if LCD_DEPTH > 1 || LCD_REMOTE_DEPTH > 1 +#endif +#if LCD_DEPTH > 1 if (!strcmp(name, "backdrop")) { - if (value[0] != '-') - if (checkwps_loader(value + 1, 2)) - return false; + if (checkwps_loader(value, NULL, 2)) /* No default path */ + return false; } else #endif if (!strcmp(name, "wps")) { - if (value[0] != '-') - if (checkwps_loader(value + 1, 2)) - return false; + if (checkwps_loader(value, WPS_DIR, 2)) + return false; } else if (!strcmp(name, "sbs")) { - if (value[0] != '-') - if (checkwps_loader(value + 1, 2)) - return false; + if (checkwps_loader(value, SBS_DIR, 2)) + return false; } else if (!strcmp(name, "fms")) { - if (value[0] != '-') - if (checkwps_loader(value + 1, 2)) - return false; + if (checkwps_loader(value, SBS_DIR, 2)) + return false; } else if (!strcmp(name, "rwps")) { - if (value[0] != '-') - if (checkwps_loader(value + 1, 2)) - return false; + if (checkwps_loader(value, WPS_DIR, 2)) + return false; } else if (!strcmp(name, "rsbs")) { - if (value[0] != '-') - if (checkwps_loader(value + 1, 2)) - return false; + if (checkwps_loader(value, SBS_DIR, 2)) + return false; } else if (!strcmp(name, "rfms")) { - if (value[0] != '-') - if (checkwps_loader(value + 1, 2)) - return false; + if (checkwps_loader(value, SBS_DIR, 2)) + return false; } + else if (!strcmp(name, "iconset")) + { + if (checkwps_loader(value, ICON_DIR, 2)) + return false; + } + else if (!strcmp(name, "viewers iconset")) + { + if (checkwps_loader(value, ICON_DIR, 2)) + return false; + } +#ifdef HAVE_REMOTE_LCD + else if (!strcmp(name, "remote iconset")) + { + if (checkwps_loader(value, ICON_DIR, 2)) + return false; + } +#endif + if (!string_to_cfg(name, value, &theme_changed)) { if (strcmp(name, "fms") && strcmp(name, "rfms")) { From bef221afd70454d948b21e9e6bc2dfd43002b273 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 2 Oct 2026 17:50:28 -0400 Subject: [PATCH 338/408] checkwps: Also validate 'filetype colours' Change-Id: I6a3dd1b08afb1037e7315fbf820dada3d108d909 --- tools/checkwps/checkwps.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/checkwps/checkwps.c b/tools/checkwps/checkwps.c index 60a72cfe24..a62105fc12 100644 --- a/tools/checkwps/checkwps.c +++ b/tools/checkwps/checkwps.c @@ -413,6 +413,13 @@ bool settings_load_config(const char* file, bool apply) return false; } #endif +#ifdef HAVE_LCD_COLOR + else if (!strcmp(name, "filetype colours")) + { + if (checkwps_loader(value, THEME_DIR, 2)) + return false; + } +#endif if (!string_to_cfg(name, value, &theme_changed)) { From e430ecb6073bd5b74fa50d988f558f143524ec3d Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Wed, 30 Sep 2026 09:29:02 -0400 Subject: [PATCH 339/408] imageviewer/jpegp: don't crash or hang on damaged files On colour targets the image viewer hands every file its own decoder rejects to jpegp, including damaged ones, but jpegp barely checks its input. Corrupt and truncated files crashed or hung it: - At the end of the file GETC() kept returning stale bytes, so marker searches and table reads never ended. Feed EOI markers (FF D9) instead, which ends every loop, and stop calling read() there. This state is reset in OPEN(): the overlay loader does not clear .bss. - A file ending before any scan decoded as a blank image. Report it as corrupt instead. - Out of range header values were used as array indexes: Huffman and conditioning table IDs, Huffman table sizes, sampling factors, scan component counts and spectral selection. Reject them, and frames of zero width or with no components. - Invalid Huffman codes walked past the code length table, run lengths wrote past coefficient 63, and huge coefficients indexed past the IDCT clamp table. Bound all three. - An odd DAC segment length never ended its loop. - The coefficient buffer size could overflow an int. Co-Authored-By: Claude Opus 5.5 Change-Id: I49465f38d283e159274d2f381029280cea42a8f1 --- apps/plugins/imageviewer/jpegp/BUFFILEGETC.c | 19 ++++++++++-- apps/plugins/imageviewer/jpegp/idct.c | 5 +++- apps/plugins/imageviewer/jpegp/jpeg81.c | 31 +++++++++++++++++--- apps/plugins/imageviewer/jpegp/jpeg81.h | 2 ++ 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/apps/plugins/imageviewer/jpegp/BUFFILEGETC.c b/apps/plugins/imageviewer/jpegp/BUFFILEGETC.c index a833851b1b..47a5087ef0 100644 --- a/apps/plugins/imageviewer/jpegp/BUFFILEGETC.c +++ b/apps/plugins/imageviewer/jpegp/BUFFILEGETC.c @@ -9,14 +9,25 @@ static unsigned char buff[256]; //TODO: Adjust it... static int length = 0; static int cur_buff_pos = 0; static int file_pos = 0; +/* Past the end of the file. Overlays are loaded without clearing their + .bss, so these are set in OPEN() rather than relied on to start at 0. */ +static bool at_eof; /* read() found the end: don't call it again */ +static unsigned eof_bytes; /* bytes fed since: alternate FF, D9 */ extern int GETC(void) { if (cur_buff_pos >= length) { - length = rb->read(fd, buff, sizeof(buff)); - file_pos += length; + length = at_eof ? 0 : rb->read(fd, buff, sizeof(buff)); cur_buff_pos = 0; + if (length <= 0) + { /* past the end of a damaged file: feed EOI markers (FF D9) + so every loop in the decoder ends instead of spinning */ + at_eof = true; + length = 0; + return (eof_bytes++ & 1) ? 0xD9 : 0xFF; + } + file_pos += length; } return buff[cur_buff_pos++]; @@ -56,11 +67,13 @@ extern void SEEK(int d) } file_pos = rb->lseek(fd, (cur_buff_pos - length) + d, SEEK_CUR); cur_buff_pos = length = 0; + at_eof = false; } extern void POS(int d) { cur_buff_pos = length = 0; + at_eof = false; file_pos = d; rb->lseek(fd, d, SEEK_SET); } @@ -77,6 +90,8 @@ extern void *OPEN(char *f) memset(buff, 0, sizeof(buff)); printf("Opening %s\n", f); cur_buff_pos = length = file_pos = 0; + at_eof = false; + eof_bytes = 0; fd = rb->open(f,O_RDONLY); if ( fd < 0 ) diff --git a/apps/plugins/imageviewer/jpegp/idct.c b/apps/plugins/imageviewer/jpegp/idct.c index 7db1658546..659d4650e6 100644 --- a/apps/plugins/imageviewer/jpegp/idct.c +++ b/apps/plugins/imageviewer/jpegp/idct.c @@ -129,6 +129,9 @@ extern void idct_sq(short *coef, int *sq) for (i=0; i<8; i++) idct1(R+i*8, C+i); for (i=0; i<8; i++) idct1(C+i*8, R+i); - for (i=0; i<64; i++) coef[i] = CLIP[ R[i] >> 15 ]; + for (i=0; i<64; i++) { // clamp: corrupt data can exceed the CLIP table + int v= R[i] >> 15; + coef[i] = CLIP[ v < -256 ? -256 : v > 511 ? 511 : v ]; + } } diff --git a/apps/plugins/imageviewer/jpegp/jpeg81.c b/apps/plugins/imageviewer/jpegp/jpeg81.c index b4849da6f7..05addec626 100644 --- a/apps/plugins/imageviewer/jpegp/jpeg81.c +++ b/apps/plugins/imageviewer/jpegp/jpeg81.c @@ -148,8 +148,11 @@ static int ReadDiff(struct JPEGD *j, int s) // JPEG magnitude stuff. One way to static int ReadHuffmanCode(struct JPEGD *j, int *pb) // index into the sym-table { - int v= GetBit(j); - while ( v >= *pb ) v= 2*v + GetBit(j) - *pb++; + int v= GetBit(j), n= 16; + while ( v >= *pb ) { + if (!--n) return 0; // no code of up to 16 bits: corrupt data + v= 2*v + GetBit(j) - *pb++; + } return v; } @@ -180,6 +183,7 @@ static void ac_decode_huff(struct JPEGD *j, struct COMP *sc, TCOEF *coef) }//else ZRL } k+=r; + if (k > j->Se) return; // corrupt data coef[k]= s; if (k==j->Se) return; } @@ -207,7 +211,8 @@ static void ac_succ_huff(struct JPEGD *j, struct COMP *sc, TCOEF *coef) break; }//else ZRL } - for (; ;k++) if (!ac_refine(j, coef+k)) if (!r--) break; + for (; k <= j->Se; k++) if (!ac_refine(j, coef+k)) if (!r--) break; + if (k > j->Se) return; // corrupt data coef[k]= s; if (k==j->Se) return; } @@ -222,6 +227,7 @@ static void du_sequential_huff(struct JPEGD *j, struct COMP *sc, TCOEF *coef) dc_decode_huff(j, sc, coef); for (k=1; (s=sc->ACS[ReadHuffmanCode(j, sc->ACB)]); k++) { // EOB? k+= s>>4; + if (k > 63) return; // corrupt data if (s==0xf0) continue; // ZRL coef[k]= ReadDiff(j, s&15); if (k==63) return; @@ -635,6 +641,8 @@ static int set_dim(struct JPEGD *j, int d) // d= 1 (LL) or 8 (DCT) C->Hi= C->Vi>>4; C->Vi&= 15; C->Qi= GETC(); + if (C->Hi < 1 || C->Hi > 4 || C->Vi < 1 || C->Vi > 4 || C->Qi > 3) + return -1; // corrupt if ( C->Hi > j->Hmax ) j->Hmax = C->Hi; if ( C->Vi > j->Vmax ) j->Vmax = C->Vi; @@ -709,12 +717,13 @@ extern enum JPEGENUM JPEGDecode(struct JPEGD *j) int La= GETWbi(); printf("DAC\n"); printf(" Arithmetic Conditioning\n parameters:\n"); - for (La-=2; La; La-=2) + for (La-=2; La > 1; La-=2) { int CB= GETC(); int Tc= CB>>4; int Tb= CB&15; int Cs= GETC(); + if (Tb > 3) return JPEGENUMERR_CORRUPT; if (Tc) // AC { printf(" AC%d Kx=%d\n", Tb, Cs); @@ -744,6 +753,7 @@ extern enum JPEGENUM JPEGDecode(struct JPEGD *j) int CH= GETC(); int Tc= CH>>4; int Th= CH&15; + if (Tc > 1 || Th > 3) return JPEGENUMERR_CORRUPT; int *B= j->HTB[Tc][Th]; unsigned char *S= j->HTS[Tc][Th]; printf(" %s%d\n", Tc?"AC":"DC", Th); @@ -755,6 +765,7 @@ extern enum JPEGENUM JPEGDecode(struct JPEGD *j) B[i]= N; // running total } printf("\n"); + if (N > 256 || 17 + N > Lh) return JPEGENUMERR_CORRUPT; printf(" S: %d symbol bytes\n", N); for (i=0; iNf>4) return JPEGENUMERR_COMP4; + if (!j->Nf) return JPEGENUMERR_CORRUPT; if (!j->Y) return JPEGENUMERR_ZEROY; // I have no idea about this DNL stuff + if (!j->X) return JPEGENUMERR_CORRUPT; j->SOF= marker; if ( (j->SOF&3)==3 ) // LOSSLESS-mode { int TotalDU= set_dim(j, 1); // for malloc: in samples as coeff; + if (TotalDU < 0) return JPEGENUMERR_CORRUPT; + if (TotalDU > 0x7fffffff / (int)sizeof(DU)) return JPEGENUMERR_MALLOC; if (j->SOF > 0xC8) { // arithmetic: @@ -819,6 +834,8 @@ extern enum JPEGENUM JPEGDecode(struct JPEGD *j) else // DCT-mode { int TotalDU= set_dim(j, 8); // for malloc in DU; + if (TotalDU < 0) return JPEGENUMERR_CORRUPT; + if (TotalDU > 0x7fffffff / (int)sizeof(DU)) return JPEGENUMERR_MALLOC; printf(" %d MCU (%d x %d)\n", j->mcu_total, j->mcu_width, j->mcu_height); @@ -857,6 +874,7 @@ extern enum JPEGENUM JPEGDecode(struct JPEGD *j) else if ( marker == 0xD9 ) // EOI { printf("EOI\n"); + if (!j->scans) return JPEGENUMERR_CORRUPT; // no image data return JPEGENUM_OK; } else if ( marker == 0xDA ) // SOS @@ -865,6 +883,8 @@ extern enum JPEGENUM JPEGDecode(struct JPEGD *j) GETWbi(); //Ls printf("SOS\n"); j->Ns= GETC();//Ns + j->scans++; + if (j->Ns < 1 || j->Ns > j->Nf) return JPEGENUMERR_CORRUPT; printf(" Ns: %d (%s scan)\n", j->Ns, (j->Ns>1)?"Interleaved":"Single"); for (ci=0; ciNs; ci++) @@ -874,6 +894,7 @@ extern enum JPEGENUM JPEGDecode(struct JPEGD *j) int T= GETC(); int Td= T>>4; int Ta= T&15; + if (Td > 3 || Ta > 3) return JPEGENUMERR_CORRUPT; printf(" Cs=%d Td=%d Ta=%d\n", Cs, Td, Ta); {// safe search @@ -908,6 +929,8 @@ extern enum JPEGENUM JPEGDecode(struct JPEGD *j) j->Ah= j->Al>>4; j->Al&= 15; j->Al2= 1<Al;//pre-computed + if (j->Se > 63 || j->Ss > j->Se || j->Al > 13) + return JPEGENUMERR_CORRUPT; printf(" %s: %d\n", ((j->SOF&3)==3)?"Px":"Ss", j->Ss); printf(" Se: %d\n", j->Se); diff --git a/apps/plugins/imageviewer/jpegp/jpeg81.h b/apps/plugins/imageviewer/jpegp/jpeg81.h index 8d9c2a04a3..a5b23abf3a 100644 --- a/apps/plugins/imageviewer/jpegp/jpeg81.h +++ b/apps/plugins/imageviewer/jpegp/jpeg81.h @@ -32,6 +32,7 @@ enum JPEGENUM { JPEGENUMERR_MARKERDNL, // DNL marker found (not supported) JPEGENUMERR_ZEROY, // Y in SOFn is zero (DNL?) JPEGENUMERR_COMPNOTFOUND, // Scan component selector (Csj) not found among Component identifiers (Ci) + JPEGENUMERR_CORRUPT, // a header value out of range }; #include @@ -105,6 +106,7 @@ struct JPEGD { // The JPEG DECODER OBJECT void *jpeg_mem; // <-- free me int Hmax, Vmax; // for conversion + int scans; // scans decoded: none means no image data bool jfif; // saw a JFIF APP0 marker unsigned char adobe; // Adobe APP14 transform flag + 1, 0 if none int mcu_width; From 0ed3734e5e34ea6c47ece2ef2c7c8a0fe0fcf325 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Sat, 19 Sep 2026 17:48:20 -0400 Subject: [PATCH 340/408] flac: cope with frames larger than one buffer request The frame decoder reads from one flat buffer and cannot refill it, but request_buffer() only guarantees 32KiB of contiguous data (the buffering guard area). Frames that can be larger than that, such as high resolution or poorly compressible streams (FLAC decoder testbench file 31), could be handed to the decoder truncated, which read past the end of the data and lost sync. When a request returns less than the largest frame the stream can contain (STREAMINFO max framesize, or a bound from block size, channels and bit depth) and it is not the end of the file, copy the frame into a private static buffer and decode from that. Streams whose frames always fit never touch the buffer and pay one comparison per frame. The buffer is 64KiB, or sized for the 4608 sample blocks of memory limited targets, and is left out entirely when MEMORYSIZE is 2MB or less. Also fail with a codec error, instead of advancing past the data, if a decoded frame consumed more bytes than were provided. Co-Authored-By: Claude Sonnet 5 Change-Id: Ib4cf85ad513f8e096f73d13b4374d072e1745cb2 --- lib/rbcodec/codecs/flac.c | 62 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/lib/rbcodec/codecs/flac.c b/lib/rbcodec/codecs/flac.c index dc3c4ba06f..c2fd70c896 100644 --- a/lib/rbcodec/codecs/flac.c +++ b/lib/rbcodec/codecs/flac.c @@ -75,6 +75,22 @@ static struct FLACseekpoints seekpoints[MAX_SUPPORTED_SEEKTABLE_SIZE]; static int nseekpoints; static int8_t *bit_buffer; +/* Frame copy buffer for flac_request_frame(). Targets with 2MB of RAM or + less (e.g. the Clip v1) cannot spare it and rely on the overrun check in + codec_run() alone. */ +#if MEMORYSIZE > 2 +#define FLAC_FRAME_COPY +#if MAX_BLOCKSIZE > 4608 +#define FRAME_COPY_SIZE MAX_FRAMESIZE +#else +/* Blocks are capped at 4608 samples here (see bitstream.h): size for the + largest stereo frame, 24 bit with verbatim subframes, plus headers. */ +#define FRAME_COPY_SIZE (MAX_BLOCKSIZE * 2 * 3 + 64) +#endif +/* Largest frame the current stream can contain */ +static size_t frame_bound; +static uint8_t frame_copy[FRAME_COPY_SIZE]; +#endif static size_t buff_size; static bool flac_init(FLACContext* fc, int first_frame_offset) @@ -209,6 +225,17 @@ static bool flac_init(FLACContext* fc, int first_frame_offset) } if (found_streaminfo) { +#ifdef FLAC_FRAME_COPY + frame_bound = fc->max_framesize; + if (frame_bound == 0) { + /* stream did not encode max frame size, assume worst case of + verbatim subframes plus headers */ + frame_bound = ((size_t)fc->max_blocksize * fc->channels * fc->bps + + 7) / 8 + 64; + } + if (frame_bound > FRAME_COPY_SIZE) + frame_bound = FRAME_COPY_SIZE; +#endif /* length is 0 when STREAMINFO has no total sample count */ fc->bitrate = fc->length ? ((int64_t) (fc->filesize-fc->metadatalength) * 8) / fc->length : 0; @@ -218,6 +245,29 @@ static bool flac_init(FLACContext* fc, int first_frame_offset) } } +/* request_buffer() only guarantees 32KiB of contiguous data, but a FLAC frame + can be larger, and the frame decoder cannot refill its buffer mid-frame. + When a request returns less than frame_bound before the end of the file, + the frame is copied into frame_copy and decoded from there. Frames + normally fit, so the usual cost is one comparison per frame. */ +#ifdef FLAC_FRAME_COPY +static void *flac_request_frame(size_t *len, size_t reqsize) +{ + void *buf = ci->request_buffer(len, reqsize); + + if (*len >= frame_bound || *len >= (size_t)(ci->filesize - ci->curpos)) + return buf; + + off_t pos = ci->curpos; + size_t got = ci->read_filebuf(frame_copy, frame_bound); + ci->seek_buffer(pos); + *len = got; + return frame_copy; +} +#else +#define flac_request_frame(len, reqsize) ci->request_buffer(len, reqsize) +#endif + /* Synchronize to next frame in stream - adapted from libFLAC 1.1.3b2 */ static bool frame_sync(FLACContext* fc) { unsigned int x = 0; @@ -256,7 +306,7 @@ static bool frame_sync(FLACContext* fc) { /* Advance and init bit buffer to the new frame. */ ci->advance_buffer((get_bits_count(&fc->gb)-16)>>3); /* consumed bytes */ - bit_buffer = ci->request_buffer(&buff_size, MAX_FRAMESIZE+16); + bit_buffer = flac_request_frame(&buff_size, MAX_FRAMESIZE+16); init_get_bits(&fc->gb, bit_buffer, buff_size*8); /* Decode the frame to verify the frame crc and @@ -503,7 +553,7 @@ enum codec_status codec_run(void) ci->set_elapsed(elapsedtime); /* The main decoding loop */ - buf = ci->request_buffer(&bytesleft, MAX_FRAMESIZE); + buf = flac_request_frame(&bytesleft, MAX_FRAMESIZE); while (bytesleft) { long action = ci->get_command(¶m); @@ -515,7 +565,7 @@ enum codec_status codec_run(void) if (flac_seek(&fc,(uint32_t)(((uint64_t)param *ci->id3->frequency)/1000))) { /* Refill the input buffer */ - buf = ci->request_buffer(&bytesleft, MAX_FRAMESIZE); + buf = flac_request_frame(&bytesleft, MAX_FRAMESIZE); } ci->set_elapsed(param); @@ -528,6 +578,10 @@ enum codec_status codec_run(void) return CODEC_ERROR; } consumed=fc.gb.index/8; + if (res == 0 && consumed > (int)bytesleft) { + LOGF("FLAC: Frame overran its %d bytes of data\n", (int)bytesleft); + return CODEC_ERROR; + } #if defined(LOGF_ENABLE) frame++; #endif @@ -545,7 +599,7 @@ enum codec_status codec_run(void) ci->advance_buffer(consumed); - buf = ci->request_buffer(&bytesleft, MAX_FRAMESIZE); + buf = flac_request_frame(&bytesleft, MAX_FRAMESIZE); } LOGF("FLAC: Decoded %lu samples\n",(unsigned long)samplesdone); From 50d8c4a58e5801799c83858c9d55885ae16b92c1 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 4 Oct 2026 13:01:34 -0400 Subject: [PATCH 341/408] =?UTF-8?q?FS#14029=20-=20Updated=20Vietnamese=20T?= =?UTF-8?q?ranslation=20(Chu=20Kh=C3=A1nh=20H=E1=BA=A1nh)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I6f5807d76989375b6318d02fd0e42f8beaa488fa --- apps/lang/vietnamese.lang | 46 ++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/apps/lang/vietnamese.lang b/apps/lang/vietnamese.lang index 2bc418cdcc..5b93b54f84 100644 --- a/apps/lang/vietnamese.lang +++ b/apps/lang/vietnamese.lang @@ -9139,15 +9139,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Khoảng cắt âm trầm" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Khoảng cắt âm trầm" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Khoảng cắt âm trầm" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Khoảng cắt âm trầm" @@ -9156,15 +9156,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Khoảng cắt âm cao" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Khoảng cắt âm cao" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Khoảng cắt âm cao" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Khoảng cắt âm cao" @@ -17125,3 +17125,37 @@ *: "Kiểu bộ lọc" + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "Đầu ra Video Composite" + + + *: none + videoout: "Đầu ra Video Composite" + + + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "Lối tắt Nháy Màn hình" + + + *: none + touchscreen: "Lối tắt Nháy Màn hình" + + From ecdeb02dda6dbb94c1c3b01b8406203eda225f9f Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 27 Sep 2026 19:48:51 -0400 Subject: [PATCH 342/408] FS#14005 - Use a 44.1KHz floor when guessing playback frequency Regression introduced in f87ff3a9b, which made it possible for audio playback to request a freq under 44.1KHz, instead of treating 44.1 as a floor (and upsampling) However, there is a report of 22KHz files playing back distorted on an imx233 target. IMO a 44KHz floor is reasonable, but this bug is a symptom of something deeper. Perhaps the mp3 codec isn't doing the right thing, or there's an issue in the pcm mixer somewhere, or the imx233 codec doesn't properly handle 22KHz? Further investigation is warranted. Change-Id: I751ce05f8605de7f90d3eb7b3c98873487df438b --- apps/playback.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/playback.c b/apps/playback.c index abb50345f7..d69b28cd2c 100644 --- a/apps/playback.c +++ b/apps/playback.c @@ -1258,7 +1258,7 @@ void allocate_playback_log(void) playback_log_handle = core_alloc(PLAYBACK_LOG_BUFSZ); if (playback_log_handle > 0) { - DEBUGF("%s Allocated %d bytes\n", __func__, PLAYBACK_LOG_BUFSZ); + DEBUGF("%s Allocated %d bytes\n", __func__, PLAYBACK_LOG_BUFSZ); char *buf = core_get_data(playback_log_handle); buf[0] = '\0'; } @@ -2995,7 +2995,7 @@ static void audio_on_track_changed(void) static void audio_start_playback(const struct audio_resume_info *resume_info, unsigned int flags) { -/* +/* * Refuse to start playback if usb audio is active. See gui_wps_show() for * a splash message to the user. * NOTE: if USBAudio ever gets its own DSP channel, this block can go away! @@ -4270,7 +4270,8 @@ static unsigned long audio_guess_frequency(struct mp3entry *id3) have_44 = true; if (caps->samprs[i] == SAMPR_48) have_48 = true; - if (id3->frequency == caps->samprs[i]) + /* Always upscale to 44.1 or better */ + if (id3->frequency >= SAMPR_44 && id3->frequency == caps->samprs[i]) return id3->frequency; } unsigned long fallback = (id3->frequency % 4000) ? SAMPR_44 : SAMPR_48; From f86be7dc42f68442064b8d10d0c9b366601c630b Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Sun, 4 Oct 2026 22:46:38 -0400 Subject: [PATCH 343/408] test_codec: fix stale results screen and scroll the log The results were drawn right after backlight_on(), which only queues a request to the backlight thread. lcd_update() does nothing while the LCD is off, and the plugin then blocked waiting for a key without updating again, so the display could keep showing the last progress line. Refresh the display periodically while waiting for a key. Also scroll the log up when the screen is full instead of wrapping around to the top and overwriting old lines, which made the output hard to read when testing a whole folder. Co-Authored-By: Claude Opus 5.5 --- apps/plugins/test_codec.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/apps/plugins/test_codec.c b/apps/plugins/test_codec.c index 7bd69369cc..d706c2eced 100644 --- a/apps/plugins/test_codec.c +++ b/apps/plugins/test_codec.c @@ -21,6 +21,7 @@ #include "lib/pluginlib_touchscreen.h" #include "lib/pluginlib_exit.h" #include "lib/pluginlib_actions.h" +#include "lib/xlcd.h" /* this set the context to use with PLA */ static const struct button_mapping *plugin_contexts[] = { pla_main_ctx }; @@ -41,6 +42,7 @@ static const struct opt_items boost_settings[2] = { /* Log functions copied from test_disk.c */ static int line = 0; static int max_line = 0; +static int line_height = 0; static int log_fd = -1; static void log_close(void) @@ -51,11 +53,10 @@ static void log_close(void) static bool log_init(bool use_logfile) { - int h; char logfilename[MAX_PATH]; - rb->lcd_getstringsize("A", NULL, &h); - max_line = LCD_HEIGHT / h; + rb->lcd_getstringsize("A", NULL, &line_height); + max_line = LCD_HEIGHT / line_height; line = 0; rb->lcd_clear_display(); rb->lcd_update(); @@ -73,12 +74,17 @@ static bool log_init(bool use_logfile) static void log_text(char *text, bool advance) { + if (line >= max_line) + { + /* Screen is full - scroll up to make room for this line */ + xlcd_scroll_up(line_height); + line = max_line - 1; + } rb->lcd_puts(0, line, text); rb->lcd_update(); if (advance) { - if (++line >= max_line) - line = 0; + line++; if (log_fd >= 0) rb->fdprintf(log_fd, "%s\n", text); } @@ -829,7 +835,11 @@ void plugin_quit(void) else #endif do { - btn = pluginlib_getaction(TIMEOUT_BLOCK, plugin_contexts, + /* lcd_update() does nothing while the LCD is off, and the + backlight_on() request is asynchronous, so the results may + not have reached the display yet - keep refreshing it */ + rb->lcd_update(); + btn = pluginlib_getaction(HZ/2, plugin_contexts, ARRAYLEN(plugin_contexts)); exit_on_usb(btn); } while ((codec_action != CODEC_ACTION_HALT) From 13553bda062322f9b35579430e54c49da1af174f Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Mon, 5 Oct 2026 09:48:30 +0200 Subject: [PATCH 344/408] radio: redraw the FM screen after the autoscan question Entering the FM screen with no presets asks whether to scan for them. The question and the scan clear the screen, after fms_fix_displays() had shown the skin's backdrop, and nothing showed it again: the skin redraws only its viewports, so the backdrop stayed missing everywhere else - the header bar of CabbieV2's FM screen among it. Leave the FM screen for the question and the scan and enter it again after, as for the other screens shown from it. Co-Authored-By: Claude Opus 5.5 Change-Id: I7caadf3ac2a03f2825176084d8969ae6e59af471 --- apps/radio/radio.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/radio/radio.c b/apps/radio/radio.c index 9b68c948c2..417164fec2 100644 --- a/apps/radio/radio.c +++ b/apps/radio/radio.c @@ -402,8 +402,16 @@ void radio_screen(void) (radio_status == FMRADIO_PAUSED) ? SRCF_FMRADIO_PAUSED : SRCF_FMRADIO_PLAYING); - if(radio_preset_count() < 1 && yesno_pop(ID2P(LANG_FM_FIRST_AUTOSCAN))) - presets_scan(NULL); + /* The question and the scan clear the screen: set it up again after, + * as for the other screens shown from here - the skin redraws only + * its viewports, and its backdrop would stay missing elsewhere. */ + if(radio_preset_count() < 1) + { + fms_fix_displays(FMS_EXIT); + if(yesno_pop(ID2P(LANG_FM_FIRST_AUTOSCAN))) + presets_scan(NULL); + fms_fix_displays(FMS_ENTER); + } preset_set_current(preset_find(curr_freq)); if(radio_current_preset() != -1) From cc362a8ecd64ec7f06b0a8c7f2173ab73dda440e Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Mon, 5 Oct 2026 09:48:30 +0200 Subject: [PATCH 345/408] buildzip: install wps/ subfolders with a dot in their name make install skipped every wps/ subfolder whose name has a dot in it: the test meant for "." and ".." matched a dot anywhere. A theme named with one lost its images. Co-Authored-By: Claude Opus 5.5 Change-Id: Ic225960349e5d73ae4defa7524fa2cee5066e920 --- tools/buildzip.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/buildzip.pl b/tools/buildzip.pl index b8f9c12530..99fb04cad2 100755 --- a/tools/buildzip.pl +++ b/tools/buildzip.pl @@ -222,7 +222,7 @@ sub make_install { foreach my $_dir (@files) { my $dir = "wps/" . $_dir; - if ( -d "$src/$dir" && $_dir !~ /\.\.?/) { + if ( -d "$src/$dir" && $_dir !~ /^\.\.?$/) { unless (glob_mkdir("$userdir/$dir")) { return 0; } From b0ad97add81a24a0105424d760529cebdadd890f Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Mon, 5 Oct 2026 09:58:10 +0200 Subject: [PATCH 346/408] wpsbuild: ship the fonts a skin loads with %Fl A theme's own font is converted into the build; fonts its skins load themselves with %Fl were not, and needed the font package - or the skin fell back to another font. Convert those as well, the way the images a skin uses are copied with it. Co-Authored-By: Claude Opus 5.5 Change-Id: Ifb698f7417287954130be9b466fe3b2248c5c773 --- wps/wpsbuild.pl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/wps/wpsbuild.pl b/wps/wpsbuild.pl index ccc4cad1f4..41facaf35e 100755 --- a/wps/wpsbuild.pl +++ b/wps/wpsbuild.pl @@ -231,6 +231,13 @@ sub copywps open(SKIN, "$dir/$file"); while () { $filelist[$#filelist + 1] = $1 if (/[\(,]([^,]*?.bmp)[\),]/); + # fonts the skin loads itself ship with it, as the theme + # font does: a default theme must not need the font pack. + # A copy of $1: copythemefont()'s own matching resets it. + if (/%Fl\(\d+,([^,\)]+\.fnt)[,\)]/) { + my $skinfont = $1; + copythemefont($skinfont); + } } close(SKIN); } From 4c78e984615493609044f1aad747c333796ceca8 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Mon, 5 Oct 2026 09:58:26 +0200 Subject: [PATCH 347/408] cabbiev2: 400x240 FM screen, sharing the WPS backdrop CabbieV2 had an FM screen only for the 160x128 and 128x128 greyscale targets. Add one for 400x240, laid out like its WPS: station art and names where the album art and track info are, the frequency in the progress bar, Scan/Preset, MHz and Stereo/Mono below it, and hold, battery, volume and signal strength along the bottom. Both screens share one backdrop, its header bar empty: each draws its label, NOW PLAYING or FM RADIO, in Helvetica Bold rather than having it painted into a backdrop of its own. The shuffle and repeat placeholders, painted into the old WPS backdrop, are images drawn while those are off. Co-Authored-By: Claude Opus 5.5 Change-Id: I0331184479589cd4953bd78f4ead336f5d9b6fea --- wps/WPSLIST | 1 + wps/cabbiev2-400x240x16.fms | 94 ++++++++++++++++++ wps/cabbiev2.400x240x16.wps | 20 +++- ...400x240x16.bmp => backdrop-400x240x16.bmp} | Bin 288054 -> 288054 bytes wps/cabbiev2/repeatoff-400x240x16.bmp | Bin 0 -> 1342 bytes wps/cabbiev2/shuffleoff-400x240x16.bmp | Bin 0 -> 1846 bytes 6 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 wps/cabbiev2-400x240x16.fms rename wps/cabbiev2/{wpsbackdrop-400x240x16.bmp => backdrop-400x240x16.bmp} (88%) create mode 100644 wps/cabbiev2/repeatoff-400x240x16.bmp create mode 100644 wps/cabbiev2/shuffleoff-400x240x16.bmp diff --git a/wps/WPSLIST b/wps/WPSLIST index d9438bb77b..5e303ebc04 100644 --- a/wps/WPSLIST +++ b/wps/WPSLIST @@ -110,6 +110,7 @@ wps.112x64x1: cabbiev2.112x64x1.wps wps.96x96x(16|24|32): cabbiev2.96x96x16.wps # override implicit .fms filename +fms.400x240x(16|24|32): cabbiev2-400x240x16.fms fms.160x128x2: cabbiev2-160x128x2.fms fms.128x128x2: cabbiev2-128x128x2.fms diff --git a/wps/cabbiev2-400x240x16.fms b/wps/cabbiev2-400x240x16.fms new file mode 100644 index 0000000000..9e5199efca --- /dev/null +++ b/wps/cabbiev2-400x240x16.fms @@ -0,0 +1,94 @@ +# Cabbie v2.0 +# (C) 2010-2026 The Authors (see /rockbox/wps/AUTHORS) +# Derived from "Cabbie v2.0" (C) 2010-2012 The Authors +# +# One backdrop for the WPS and the FM screen: the header label is drawn, +# rather than painted into it. +# +# Disable Status Bar +%wd +# +# Load Backdrop +%X(backdrop-400x240x16.bmp) +# +# Header font +%Fl(2,12-Adobe-Helvetica-Bold.fnt) +# +# Preload Images +%xl(lock,lock-320x240x16.bmp,0,0,2) +%xl(battery,battery-320x240x16.bmp,0,0,10) +%xl(volume,volume-320x240x16.bmp,0,0,10) +# +# Station Art/Info Viewport Conditional +# Preset present conditional +# RDS present conditional +%?C<%Vd(art)> +%?Ti<%?C<%Vd(art_preset)|%Vd(noart_preset)>> +%?tx<%?Ti<%?C<%Vd(art_preset_rds)|%Vd(noart_preset_rds)>|%?C<%Vd(art_nopreset_rds)|%Vd(noart_nopreset_rds)>>|> +# +# Header +%V(10,11,380,12,2)%Vf(0F0B00) +%acFM RADIO +# +# Frequency Bar +%V(10,162,380,15,-) +%pb(0,0,380,15,pb-400x240x16.bmp) +# +# Hold +%V(144,207,30,23,-) +%xd(lock,%mh) +# +# Battery +%V(182,207,44,23,-) +%?bp<%?bc<%xd(battery,1)|%xd(battery,2)>|%xd(battery,%bl,2)> +# +# Volume +%V(233,207,34,23,-) +%xd(volume,%pv) +# +# Signal strength +%V(277,207,113,23,1) +%ar%tr dBuV +# +# Below frequency bar Scan/Preset frequency MHz Force Mono/Mono/Stereo +%V(32,180,336,20,1) +%al%?tm<%Sx(Scan)|%Sx(Preset)> %ac%tf MHz %ar%?St(force fm mono)<%Sx(Force Mono)|%?ts<%Sx(Stereo)|%Sx(Mono)>> +# +# Station Art +%ax%Vl(art,16,32,120,120,-) +%Cl(0,0,120,120,c,c) +%Cd +# +# Station Art +# Preset present +%ax%Vl(art_preset,153,40,-,20,1) +%Vp(0,%s%al%?Ti%?Tn<%Tn|%Tf>) +%ax%Vl(art_preset,153,62,-,20,1) +%Vp(1,%s%al%?Ti%?Tn<%Tn|%Tf>) +# +%ax%Vl(art_preset_rds,153,94,-,40,1) +%s%al%ty +%s%al%tz +# +# Station Art +# Preset absent +%ax%Vl(art_nopreset_rds,153,62,-,40,1) +%s%al%ty +%s%al%tz +# +# No station art +# Preset present +%Vl(noart_preset,10,40,380,20,1) +%Vp(0,%s%ac%?Ti%?Tn<%Tn|%Tf>) +%Vl(noart_preset,10,62,380,20,1) +%Vp(1,%s%ac%?Ti%?Tn<%Tn|%Tf>) +# +%Vl(noart_preset_rds,10,94,380,40,1) +%s%ac%ty +%s%ac%tz +# +# No station art +# Preset absent +%Vl(noart_nopreset_rds,10,62,380,40,1) +%s%ac%ty +%s%ac%tz diff --git a/wps/cabbiev2.400x240x16.wps b/wps/cabbiev2.400x240x16.wps index 469ee3d2f1..e9294d32a6 100644 --- a/wps/cabbiev2.400x240x16.wps +++ b/wps/cabbiev2.400x240x16.wps @@ -1,12 +1,18 @@ # Cabbie v2.0 -# (C) 2007-2012 The Authors (see /rockbox/wps/AUTHORS) +# (C) 2007-2026 The Authors (see /rockbox/wps/AUTHORS) # Derived from "cabbie" (C) Yohann Misquitta # +# One backdrop for the WPS and the FM screen: the header label is drawn, +# rather than painted into it. +# # Disable Status Bar %wd # # Load Backdrop -%X(wpsbackdrop-400x240x16.bmp) +%X(backdrop-400x240x16.bmp) +# +# Header font +%Fl(2,12-Adobe-Helvetica-Bold.fnt) # # Preload Images %xl(A,lock-320x240x16.bmp,0,0,2) @@ -15,10 +21,16 @@ %xl(D,shuffle-320x240x16.bmp,0,0) %xl(E,repeat-320x240x16.bmp,0,0,4) %xl(F,playmode-320x240x16.bmp,0,0,5) +%xl(G,shuffleoff-400x240x16.bmp,0,0) +%xl(H,repeatoff-400x240x16.bmp,0,0) # # Album Art/Info Viewport Conditional %?C<%Vd(a)|%Vd(b)> # +# Header +%V(10,11,380,12,2)%Vf(0F0B00) +%acNOW PLAYING +# # Progress Bar %V(10,162,380,15,-) %pb(0,0,380,15,pb-400x240x16.bmp) @@ -37,11 +49,11 @@ # # Shuffle %V(277,211,37,16,-) -%?ps<%xd(D)> +%?ps<%xd(D)|%xd(G)> # # Repeat %V(320,207,18,23,-) -%?mm<|%xd(Ea)|%xd(Eb)|%xd(Ec)|%xd(Ed)> +%?mm<%xd(H)|%xd(Ea)|%xd(Eb)|%xd(Ec)|%xd(Ed)> # # Playmode %V(346,207,24,23,-) diff --git a/wps/cabbiev2/wpsbackdrop-400x240x16.bmp b/wps/cabbiev2/backdrop-400x240x16.bmp similarity index 88% rename from wps/cabbiev2/wpsbackdrop-400x240x16.bmp rename to wps/cabbiev2/backdrop-400x240x16.bmp index 3a7514f4e1e19f0698936a20165c8a35c8acf09a..3f04ea6caaff5d0dc58a088d3ae081725eb5c803 100644 GIT binary patch delta 3171 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 diff --git a/wps/cabbiev2/repeatoff-400x240x16.bmp b/wps/cabbiev2/repeatoff-400x240x16.bmp new file mode 100644 index 0000000000000000000000000000000000000000..675a917f21fde3c87a1c9e4760257ad015571cf4 GIT binary patch literal 1342 zcmZ{kYfl$Z|3%^%~X} zARzqVoGqjfDrqUUx3^hrTAmjxv9$)b0w2!wAuhZ$+Hc3b*UZ-Oy{@kCtHbWP_$pP|pE@FFPSvfXYq&S$)SuXl5E)4jS%B;sgFq7ur0vY>0U$K~ba zU@*A8zNQ#La4JlP!(p%2Bfl9*5*0ii&&*CV8trzw@bG|R#TyKU&Yjcm_a$$pu2W7J zdOH0xnM@gm!4c6A)DzH(LeRpuqucR#eBJNI;xX*cAL)utzyUGQ6@22J^X4T4?dUkS zkB^VUfW9-yiAeo zb-P#l`}@K5pb!Bw!1;}TkEheux7W9S@%sA47Lsv)e?J_JXf^&j3}a(sgVJJ}EFYFI znaGdnbo&1O4v9IrkQe&!yoTZRnr3i!Z%-D*8NOvnPORFVtg_4#n6?t>)4 yo=Gn+`CyM{p3=5nEM=wo%RBg9{C2IOx5 literal 0 HcmV?d00001 diff --git a/wps/cabbiev2/shuffleoff-400x240x16.bmp b/wps/cabbiev2/shuffleoff-400x240x16.bmp new file mode 100644 index 0000000000000000000000000000000000000000..1a0f325ed9f351ae4f3753d61e5cffbd1a21905a GIT binary patch literal 1846 zcmZ{k3sV|N6oqHYVTR%r6e}PPU9)FiDDLY^GElG?bqGF8Y_EY zdb)4lbI$GC-BTMBOifyW_LS!XPmt#iEkcVi|4%?Y%AsjH8`F36P4~a|YgafN{#^Ln zY&JWcPPf~w*X!AAE)aP48b`c`8Ea za=H9vW2Mp;RFTBt()G|@h-|P_I&B)Jq!3JkN|DL8(EDGLp@Bf)_V%{jZo`h=8>6bE z5|77YvDk1p?DcvKi^XCv82l}*7L7(N%Q`>5piQOH@p!`P_31hp3v=nNvomWj7_c}h zi6KCr3~Ke7^01dTH#fm|!Dh47YPZ;6Bvemh@fiI6{ysr|zaNT0f1vh;Vln(PnPksMks{+~3(56Xh&KQ#CoO%LsYfgt6NyXH zKyLMvYOmj$OeW-En&$5AE@?t1uRxbbN(b=h=*XVfFap6yLRN_VTYHpWNy2b(apCcJ z0E+d^W;0wAlzQ@qoh2ff&*wj7KG9K+y4h+LU^78VMsrh`aX1__3 z*smfQlP_kS8PGXpPaYp@kw_$+=FX%!pCre`a=Cnbe0+F#_%4l(8ZH(Kc8jv0hxvRy zYU4PL(i8kLE#Qg)6a=71>-zedtpTM;EDd+mK+>5z2s~pis*$hfXPi)T`7 zk{>^2xEt&rTSeKS{_Dq&mzS5PrzaqY4Tqz2D#cNdLZ&8#1X$tuQjz9z`LJ58kOa9} zJ&`ZYYPEuZKnn1d!SH%Kp-_nP%nq^2pMU=ZcYl9Rsrc|kB4M=}03bE-+L~eVUAQh5 zlmY_79PALde>p-QpMad2~hwDUUtPCKKOeW*?dVh^V&&}_Rxj7VPmX*zBbGh8P zW$_IXLF&JGneH`CzTDgps)rs+sd`$<)(VWCji4F+X|h*ALpA$!`ty`(cW literal 0 HcmV?d00001 From 5d8ce62b45bdcbb74ef3dcd3d277b65eba6eb2ac Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Sun, 4 Oct 2026 23:46:57 -0400 Subject: [PATCH 348/408] codecs: link libarm_support for its division routines lib/arm_support/support-arm.S was written to replace libgcc's division for ARM, and bff5a35c3c (FS#10943, 2010) added it to the core, the plugin library and the codec library alike. When 1501df045f (2013) replaced EXTRA_LIBS with explicit lists, plugins kept it and codecs did not, and they have taken their division from libgcc since. With the gcc 4.4 toolchain that cost little: its libgcc had a routine that used clz. With gcc 9.5 libgcc has no soft-float ARMv5 variant, so ARMv5 targets get the ARMv4 routine, a shift and subtract loop of about 130 cycles a division. Monkey's Audio divides two or three times a sample in its range decoder and, without codec IRAM, does it in C. On a Clip+ it is 11% to 21% slower than 3.14 was, with over half of -c1000's decode in __udivsi3. MP2 is 4% to 7% slower. Put libarm_support back, ahead of libgcc. In perfsim's model of the Clip+ a division falls to 44 cycles, Monkey's Audio by 38%, 30% and 22% at -c1000, -c2000 and -c3000 (60.9 to 37.9 MHz at -c1000), and MP2 by 3% to 7%; nothing else moves by more than 0.6%. A Clip+ decodes -c1000 with the same checksum as before. A division by zero in a codec goes to __div0 again, and so to the firmware's handler, as it did with support-arm.S and with the old libgcc (3.14's ape.codec calls it). The gcc 9.5 libgcc returns from its own stub instead. Change-Id: I6a9a79ce8e85bca69870349a5c0823f392a578b6 Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/codecs.make | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/rbcodec/codecs/codecs.make b/lib/rbcodec/codecs/codecs.make index 53233f7f02..f0c227a136 100644 --- a/lib/rbcodec/codecs/codecs.make +++ b/lib/rbcodec/codecs/codecs.make @@ -17,8 +17,9 @@ CODECS := $(subst $(RBCODECLIB_DIR),$(RBCODEC_BLD),$(CODECS)) include $(RBCODECLIB_DIR)/codecs/lib/libcodec.make OTHER_INC += -I$(RBCODECLIB_DIR)/codecs/lib -# extra libraries -CODEC_LIBS := $(CODECLIB) $(FIXEDPOINTLIB) +# extra libraries. libarm_support has the division routines, which are much +# faster than what libgcc may supply; it is empty when not building for ARM. +CODEC_LIBS := $(CODECLIB) $(FIXEDPOINTLIB) $(ARMSUPPORTLIB) # compile flags for codecs CODECFLAGS := $(CFLAGS) $(RBCODEC_CFLAGS) -fstrict-aliasing \ From aa7ebbe057e2c6fef139fa032008edba181dfb9e Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 5 Oct 2026 01:00:12 -0400 Subject: [PATCH 349/408] libdemac: entropy state in registers, one less divide After the division routines the entropy decoder was the largest part of Monkey's Audio at the fast levels, with two things to gain. All of its state, the range coder's and the Rice parameters, was in statics, so each use was a load and each update a store: about half of the function's time on ARM7TDMI, where a load is 3 cycles. Copy it to locals for the length of a block and write it back after. The functions that take a pointer to it have to be inlined for that to work, and on ARMv4 gcc left the per-sample one out of line, so force them. The symbol was found by dividing low by help and searching the count table for the quotient. counts[n] <= low / help is the same as counts[n] * help <= low, so search with the multiply instead: the first symbols are by far the likeliest. That leaves two divisions a sample from three. MHz for real time in perfsim's models, -c1000, -c2000 and -c3000: Clip+ (ARMv5) 37.9, 53.8, 81.8 to 32.5, 48.4, 76.4 e200 (ARMv4) 45.3, 68.6, 111.4 to 35.2, 58.5, 101.3 A Clip+ measures 32.85 at -c1000 (61.4 before this and the division change), with test_codec's checksum, 1008ffab, the same as the old code's. The standalone decoder gives identical output for all four levels tested. The path for files older than 3.98 has the same change and was not tested, for want of a file. Co-Authored-By: Claude Opus 5.5 Change-Id: Idf9002a5b5f3a423f44f70443936ea5ac427cd12 --- lib/rbcodec/codecs/demac/libdemac/entropy.c | 239 ++++++++++---------- 1 file changed, 122 insertions(+), 117 deletions(-) diff --git a/lib/rbcodec/codecs/demac/libdemac/entropy.c b/lib/rbcodec/codecs/demac/libdemac/entropy.c index 44b460fe9f..347bff37eb 100644 --- a/lib/rbcodec/codecs/demac/libdemac/entropy.c +++ b/lib/rbcodec/codecs/demac/libdemac/entropy.c @@ -121,21 +121,37 @@ each function (and the RNGC macro)). for aligned reads. */ -static unsigned char* bytebuffer IBSS_ATTR_DEMAC; -static int bytebufferoffset IBSS_ATTR_DEMAC; - -static inline void skip_byte(void) +/* The decoder's state. It lives in statics between calls, but while a + block is decoded it is copied to locals, so that the compiler can keep + it in registers: with every field in memory, loads and stores were about + half of the decode time on ARM7TDMI. That only works if everything + that takes a pointer to it is inlined, which the compiler does not + always choose to do by itself. */ +#define DEMAC_INLINE static inline __attribute__((always_inline)) +struct rangecoder_t { - bytebufferoffset--; - bytebuffer += bytebufferoffset & 4; - bytebufferoffset &= 3; + uint32_t low; /* low end of interval */ + uint32_t range; /* length of interval */ + uint32_t help; /* bytes_to_follow resp. intermediate value */ + unsigned int buffer; /* buffer for input/output */ + unsigned char* bytebuffer; + int bytebufferoffset; +}; + +static struct rangecoder_t rc IBSS_ATTR_DEMAC; + +DEMAC_INLINE void skip_byte(struct rangecoder_t* rc) +{ + rc->bytebufferoffset--; + rc->bytebuffer += rc->bytebufferoffset & 4; + rc->bytebufferoffset &= 3; } -static inline int read_byte(void) +DEMAC_INLINE int read_byte(struct rangecoder_t* rc) { - int ch = bytebuffer[bytebufferoffset]; + int ch = rc->bytebuffer[rc->bytebufferoffset]; - skip_byte(); + skip_byte(rc); return ch; } @@ -150,31 +166,21 @@ static inline int read_byte(void) #define EXTRA_BITS ((CODE_BITS-2) % 8 + 1) #define BOTTOM_VALUE (TOP_VALUE >> 8) -struct rangecoder_t -{ - uint32_t low; /* low end of interval */ - uint32_t range; /* length of interval */ - uint32_t help; /* bytes_to_follow resp. intermediate value */ - unsigned int buffer; /* buffer for input/output */ -}; - -static struct rangecoder_t rc IBSS_ATTR_DEMAC; - /* Start the decoder */ -static inline void range_start_decoding(void) +DEMAC_INLINE void range_start_decoding(struct rangecoder_t* rc) { - rc.buffer = read_byte(); - rc.low = rc.buffer >> (8 - EXTRA_BITS); - rc.range = (uint32_t) 1 << EXTRA_BITS; + rc->buffer = read_byte(rc); + rc->low = rc->buffer >> (8 - EXTRA_BITS); + rc->range = (uint32_t) 1 << EXTRA_BITS; } -static inline void range_dec_normalize(void) +DEMAC_INLINE void range_dec_normalize(struct rangecoder_t* rc) { - while (rc.range <= BOTTOM_VALUE) - { - rc.buffer = (rc.buffer << 8) | read_byte(); - rc.low = (rc.low << 8) | ((rc.buffer >> 1) & 0xff); - rc.range <<= 8; + while (rc->range <= BOTTOM_VALUE) + { + rc->buffer = (rc->buffer << 8) | read_byte(rc); + rc->low = (rc->low << 8) | ((rc->buffer >> 1) & 0xff); + rc->range <<= 8; } } @@ -182,87 +188,76 @@ static inline void range_dec_normalize(void) /* tot_f is the total frequency */ /* or: totf is (code_value)1<help = UDIV32(rc->range, tot_f); + return UDIV32(rc->low, rc->help); } -static inline int range_decode_culshift(int shift) +DEMAC_INLINE int range_decode_culshift(struct rangecoder_t* rc, int shift) { - range_dec_normalize(); - rc.help = rc.range >> shift; - return UDIV32(rc.low, rc.help); + range_dec_normalize(rc); + rc->help = rc->range >> shift; + return UDIV32(rc->low, rc->help); } /* Update decoding state */ /* sy_f is the interval length (frequency of the symbol) */ /* lt_f is the lower end (frequency sum of < symbols) */ -static inline void range_decode_update(int sy_f, int lt_f) +DEMAC_INLINE void range_decode_update(struct rangecoder_t* rc, + int sy_f, int lt_f) { - rc.low -= rc.help * lt_f; - rc.range = rc.help * sy_f; + rc->low -= rc->help * lt_f; + rc->range = rc->help * sy_f; } /* Decode a byte/short without modelling */ -static inline unsigned char decode_byte(void) -{ int tmp = range_decode_culshift(8); - range_decode_update( 1,tmp); - return tmp; -} - -static inline unsigned short range_decode_short(void) -{ int tmp = range_decode_culshift(16); - range_decode_update( 1,tmp); +DEMAC_INLINE unsigned short range_decode_short(struct rangecoder_t* rc) +{ int tmp = range_decode_culshift(rc, 16); + range_decode_update(rc, 1, tmp); return tmp; } /* Decode n bits (n <= 16) without modelling - based on range_decode_short */ -static inline int range_decode_bits(int n) -{ int tmp = range_decode_culshift(n); - range_decode_update( 1,tmp); +DEMAC_INLINE int range_decode_bits(struct rangecoder_t* rc, int n) +{ int tmp = range_decode_culshift(rc, n); + range_decode_update(rc, 1, tmp); return tmp; } /* Finish decoding */ -static inline void range_done_decoding(void) -{ range_dec_normalize(); /* normalize to use up all bytes */ +DEMAC_INLINE void range_done_decoding(struct rangecoder_t* rc) +{ range_dec_normalize(rc); /* normalize to use up all bytes */ } /* - range_get_symbol_* functions based on main decoding loop in simple_d.c from + range_get_symbol() is based on the main decoding loop in simple_d.c from http://www.compressconsult.com/rangecoder/rngcod13.zip (c) Michael Schindler + + That divides low by help for the cumulative frequency cf and then looks + for the last symbol whose count is not above it. counts[n] <= cf is the + same as counts[n] * help <= low, so the division is not needed: the search + multiplies instead. The first few symbols are by far the likeliest, so + it is short. No count is above 65536 and help is below it here, so the + product cannot overflow. */ - -static inline int range_get_symbol_3980(void) +DEMAC_INLINE int range_get_symbol(struct rangecoder_t* rc, + const int* counts, const int* counts_diff) { - int symbol, cf; + int symbol; - cf = range_decode_culshift(16); + range_dec_normalize(rc); + rc->help = rc->range >> 16; - /* figure out the symbol inefficiently; a binary search would be much better */ - for (symbol = 0; counts_3980[symbol+1] <= cf; symbol++); + for (symbol = 0; (uint32_t)counts[symbol+1] * rc->help <= rc->low; + symbol++); - range_decode_update(counts_diff_3980[symbol],counts_3980[symbol]); - - return symbol; -} - -static inline int range_get_symbol_3970(void) -{ - int symbol, cf; - - cf = range_decode_culshift(16); - - /* figure out the symbol inefficiently; a binary search would be much better */ - for (symbol = 0; counts_3970[symbol+1] <= cf; symbol++); - - range_decode_update(counts_diff_3970[symbol],counts_3970[symbol]); + range_decode_update(rc, counts_diff[symbol], counts[symbol]); return symbol; } @@ -278,7 +273,7 @@ struct rice_t static struct rice_t riceX IBSS_ATTR_DEMAC; static struct rice_t riceY IBSS_ATTR_DEMAC; -static inline void update_rice(struct rice_t* rice, int x) +DEMAC_INLINE void update_rice(struct rice_t* rice, int x) { rice->ksum += ((x + 1) / 2) - ((rice->ksum + 16) >> 5); @@ -294,7 +289,8 @@ static inline void update_rice(struct rice_t* rice, int x) } } -static inline int entropy_decode3980(struct rice_t* rice) +DEMAC_INLINE int entropy_decode3980(struct rangecoder_t* rc, + struct rice_t* rice) { int base, x, pivot, overflow; @@ -302,11 +298,11 @@ static inline int entropy_decode3980(struct rice_t* rice) if (UNLIKELY(pivot == 0)) pivot=1; - overflow = range_get_symbol_3980(); + overflow = range_get_symbol(rc, counts_3980, counts_diff_3980); if (UNLIKELY(overflow == (MODEL_ELEMENTS-1))) { - overflow = range_decode_short() << 16; - overflow |= range_decode_short(); + overflow = range_decode_short(rc) << 16; + overflow |= range_decode_short(rc); } if (pivot >= 0x10000) { @@ -322,17 +318,17 @@ static inline int entropy_decode3980(struct rice_t* rice) */ lo_bits = (nbits - 16); - base_hi = range_decode_culfreq((pivot >> lo_bits) + 1); - range_decode_update(1, base_hi); + base_hi = range_decode_culfreq(rc, (pivot >> lo_bits) + 1); + range_decode_update(rc, 1, base_hi); - base_lo = range_decode_culshift(lo_bits); - range_decode_update(1, base_lo); + base_lo = range_decode_culshift(rc, lo_bits); + range_decode_update(rc, 1, base_lo); base = (base_hi << lo_bits) + base_lo; } else { /* Codepath for 16-bit streams */ - base = range_decode_culfreq(pivot); - range_decode_update(1, base); + base = range_decode_culfreq(rc, pivot); + range_decode_update(rc, 1, base); } x = base + (overflow * pivot); @@ -346,24 +342,25 @@ static inline int entropy_decode3980(struct rice_t* rice) } -static inline int entropy_decode3970(struct rice_t* rice) +DEMAC_INLINE int entropy_decode3970(struct rangecoder_t* rc, + struct rice_t* rice) { int x, tmpk; - int overflow = range_get_symbol_3970(); + int overflow = range_get_symbol(rc, counts_3970, counts_diff_3970); if (UNLIKELY(overflow == (MODEL_ELEMENTS - 1))) { - tmpk = range_decode_bits(5); + tmpk = range_decode_bits(rc, 5); overflow = 0; } else { tmpk = (rice->k < 1) ? 0 : rice->k - 1; } if (tmpk <= 16) { - x = range_decode_bits(tmpk); + x = range_decode_bits(rc, tmpk); } else { - x = range_decode_short(); - x |= (range_decode_bits(tmpk - 16) << 16); + x = range_decode_short(rc); + x |= (range_decode_bits(rc, tmpk - 16) << 16); } x += (overflow << tmpk); @@ -380,24 +377,24 @@ void init_entropy_decoder(struct ape_ctx_t* ape_ctx, unsigned char* inbuffer, int* firstbyte, int* bytesconsumed) { - bytebuffer = inbuffer; - bytebufferoffset = *firstbyte; + rc.bytebuffer = inbuffer; + rc.bytebufferoffset = *firstbyte; /* Read the CRC */ - ape_ctx->CRC = read_byte(); - ape_ctx->CRC = (ape_ctx->CRC << 8) | read_byte(); - ape_ctx->CRC = (ape_ctx->CRC << 8) | read_byte(); - ape_ctx->CRC = (ape_ctx->CRC << 8) | read_byte(); + ape_ctx->CRC = read_byte(&rc); + ape_ctx->CRC = (ape_ctx->CRC << 8) | read_byte(&rc); + ape_ctx->CRC = (ape_ctx->CRC << 8) | read_byte(&rc); + ape_ctx->CRC = (ape_ctx->CRC << 8) | read_byte(&rc); /* Read the frame flags if they exist */ ape_ctx->frameflags = 0; if ((ape_ctx->fileversion > 3820) && (ape_ctx->CRC & 0x80000000)) { ape_ctx->CRC &= ~0x80000000; - ape_ctx->frameflags = read_byte(); - ape_ctx->frameflags = (ape_ctx->frameflags << 8) | read_byte(); - ape_ctx->frameflags = (ape_ctx->frameflags << 8) | read_byte(); - ape_ctx->frameflags = (ape_ctx->frameflags << 8) | read_byte(); + ape_ctx->frameflags = read_byte(&rc); + ape_ctx->frameflags = (ape_ctx->frameflags << 8) | read_byte(&rc); + ape_ctx->frameflags = (ape_ctx->frameflags << 8) | read_byte(&rc); + ape_ctx->frameflags = (ape_ctx->frameflags << 8) | read_byte(&rc); } /* Keep a count of the blocks decoded in this frame */ ape_ctx->blocksdecoded = 0; @@ -409,13 +406,13 @@ void init_entropy_decoder(struct ape_ctx_t* ape_ctx, riceY.ksum = (1 << riceY.k) * 16; /* The first 8 bits of input are ignored. */ - skip_byte(); + skip_byte(&rc); - range_start_decoding(); + range_start_decoding(&rc); /* Return the new state of the buffer */ - *bytesconsumed = (intptr_t)bytebuffer - (intptr_t)inbuffer; - *firstbyte = bytebufferoffset; + *bytesconsumed = (intptr_t)rc.bytebuffer - (intptr_t)inbuffer; + *firstbyte = rc.bytebufferoffset; } void ICODE_ATTR_DEMAC entropy_decode(struct ape_ctx_t* ape_ctx, @@ -424,8 +421,12 @@ void ICODE_ATTR_DEMAC entropy_decode(struct ape_ctx_t* ape_ctx, int32_t* decoded0, int32_t* decoded1, int blockstodecode) { - bytebuffer = inbuffer; - bytebufferoffset = *firstbyte; + /* local copies, for the compiler to keep in registers */ + struct rangecoder_t r = rc; + struct rice_t rx = riceX, ry = riceY; + + r.bytebuffer = inbuffer; + r.bytebufferoffset = *firstbyte; ape_ctx->blocksdecoded += blockstodecode; @@ -439,25 +440,29 @@ void ICODE_ATTR_DEMAC entropy_decode(struct ape_ctx_t* ape_ctx, } else { if (ape_ctx->fileversion > 3970) { while (LIKELY(blockstodecode--)) { - *(decoded0++) = entropy_decode3980(&riceY); + *(decoded0++) = entropy_decode3980(&r, &ry); if (decoded1 != NULL) - *(decoded1++) = entropy_decode3980(&riceX); + *(decoded1++) = entropy_decode3980(&r, &rx); } } else { while (LIKELY(blockstodecode--)) { - *(decoded0++) = entropy_decode3970(&riceY); + *(decoded0++) = entropy_decode3970(&r, &ry); if (decoded1 != NULL) - *(decoded1++) = entropy_decode3970(&riceX); + *(decoded1++) = entropy_decode3970(&r, &rx); } } } if (ape_ctx->blocksdecoded == ape_ctx->currentframeblocks) { - range_done_decoding(); + range_done_decoding(&r); } + rc = r; + riceX = rx; + riceY = ry; + /* Return the new state of the buffer */ - *bytesconsumed = bytebuffer - inbuffer; - *firstbyte = bytebufferoffset; + *bytesconsumed = r.bytebuffer - inbuffer; + *firstbyte = r.bytebufferoffset; } From a6b7321d9f8b2ec4d4b810391d5eb377b25d850d Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 5 Oct 2026 01:25:09 -0400 Subject: [PATCH 350/408] libdemac: reciprocal table for the pivot division on ARMv5+ Of the two divisions a sample left in the entropy decoder, the first divides the range by the Rice pivot, which is small: below 1024 for 99.9% of the samples of a 16-bit test file and never as much as 4096. Keep a table of reciprocals, filled in as divisors turn up, and divide with a 32x32->64 multiply and one correction. Larger divisors go to the division routine as before. The other division, by help, has no such pattern. This is for ARMv5 and later without a hardware divide. ARMv4 has its own divider with a reciprocal table already, and a long multiply is slow there; its codec is unchanged. The table is 16 KB of bss. With r = (2^32 - 1) / n the estimate is the quotient or one less for any 32-bit numerator, checked against true division for every n below 4096. MHz for real time in perfsim's model of the Clip+, -c1000, -c2000 and -c3000: 32.5, 48.4, 76.4 to 30.1, 46.0, 74.0. A Clip+ measures 30.63 at -c1000, from 32.85, with the same checksum, 1008ffab. Co-Authored-By: Claude Opus 5.5 Change-Id: I555ff83d7e19919848c8fb7b8d12ecfa671a976b --- lib/rbcodec/codecs/demac/libdemac/entropy.c | 33 ++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/rbcodec/codecs/demac/libdemac/entropy.c b/lib/rbcodec/codecs/demac/libdemac/entropy.c index 347bff37eb..dc4448e34c 100644 --- a/lib/rbcodec/codecs/demac/libdemac/entropy.c +++ b/lib/rbcodec/codecs/demac/libdemac/entropy.c @@ -184,6 +184,37 @@ DEMAC_INLINE void range_dec_normalize(struct rangecoder_t* rc) } } +/* range / tot_f. tot_f is the Rice pivot, which is small (below 1024 + for nearly every sample of ordinary 16-bit audio), so where a 32x32->64 + multiply is cheap its reciprocal comes from a table that is filled in as + divisors turn up, and the division is a multiply and a correction. The + other division, by help, has no such pattern. */ +#if defined(CPU_ARM) && ARM_ARCH >= 5 && !defined(ARM_HAVE_HW_DIV) +#define RECIP_ENTRIES 4096 +/* recip[n] is (2^32 - 1) / n, which is never 0: 0 is "not worked out" */ +static uint32_t recip[RECIP_ENTRIES]; + +DEMAC_INLINE uint32_t range_div(uint32_t range, uint32_t tot_f) +{ + uint32_t r, q; + + if (UNLIKELY(tot_f >= RECIP_ENTRIES)) + return UDIV32(range, tot_f); + + r = recip[tot_f]; + if (UNLIKELY(r == 0)) + r = recip[tot_f] = UDIV32(0xffffffff, tot_f); + + /* this is the quotient or one less */ + q = ((uint64_t)range * r) >> 32; + if (range - q * tot_f >= tot_f) + q++; + return q; +} +#else +#define range_div(range, tot_f) UDIV32(range, tot_f) +#endif + /* Calculate culmulative frequency for next symbol. Does NO update!*/ /* tot_f is the total frequency */ /* or: totf is (code_value)1<help = UDIV32(rc->range, tot_f); + rc->help = range_div(rc->range, tot_f); return UDIV32(rc->low, rc->help); } From 1896c2128b6f1b9a4216059b3e7293c6c1c13d9c Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 5 Oct 2026 10:54:16 -0400 Subject: [PATCH 351/408] test_codec: keep the log if the plugin is left early A file's length and first cluster are only written to its directory entry when it is closed, and the log was closed at the end of plugin_start() alone. Leaving by USB or by power off goes through exit() instead, so a whole run's log could be left as an empty file with its data in clusters nothing pointed to. Close the log from an atexit handler, close it when a run over a folder ends, and close and reopen it after each track so that at most one result is lost if the player dies. Co-Authored-By: Claude Opus 5.5 Change-Id: I613d0ad8c637d7f48a82eb09bc310c6fcc6d77f9 --- apps/plugins/test_codec.c | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/apps/plugins/test_codec.c b/apps/plugins/test_codec.c index d706c2eced..b38ae221e0 100644 --- a/apps/plugins/test_codec.c +++ b/apps/plugins/test_codec.c @@ -44,16 +44,31 @@ static int line = 0; static int max_line = 0; static int line_height = 0; static int log_fd = -1; +static char logfilename[MAX_PATH]; static void log_close(void) { if (log_fd >= 0) rb->close(log_fd); + log_fd = -1; +} + +/* The file's length only reaches the disk when it is closed. So that what + has been logged survives the player being switched off, or its battery + running out, before the plugin is left: close the log and open it again + to carry on. */ +static void log_commit(void) +{ + if (log_fd >= 0) + { + rb->close(log_fd); + log_fd = rb->open(logfilename, O_WRONLY|O_APPEND); + } } static bool log_init(bool use_logfile) { - char logfilename[MAX_PATH]; + static bool close_at_exit = false; rb->lcd_getstringsize("A", NULL, &line_height); max_line = LCD_HEIGHT / line_height; @@ -61,8 +76,16 @@ static bool log_init(bool use_logfile) rb->lcd_clear_display(); rb->lcd_update(); + log_close(); + if (use_logfile) { - log_close(); + /* Leaving on USB or power off goes through exit(), not the end of + plugin_start(). */ + if (!close_at_exit) + { + atexit(log_close); + close_at_exit = true; + } rb->create_numbered_filename(logfilename, HOME_DIR, "test_codec_log_", ".txt", 2 IF_CNFN_NUM_(, NULL)); log_fd = rb->open(logfilename, O_RDWR|O_CREAT|O_TRUNC, 0666); @@ -1014,6 +1037,7 @@ menu: break; log_text("", true); + log_commit(); } /* Read next entry */ @@ -1022,6 +1046,9 @@ menu: rb->closedir(dir); } + + /* The run is over: nothing more goes in the log. */ + log_close(); } else { /* Just test the file */ res = test_track(parameter); From 374f7c03341f9814439f15cacb16894b66e2a5c5 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Sat, 19 Sep 2026 23:35:55 -0400 Subject: [PATCH 352/408] flac: ARM assembly for the 64-bit LPC filter Replaces flac_lpc_32_c, the wide predictor path every 24-bit stream takes, and 16-bit streams encoded at high coefficient precision. The coefficients are invariant for the whole call, but the C loop reloaded all of them for every output sample and spilled its loop bound to the stack on top of that. Orders 1-8 now keep every coefficient in a register; orders 10 and 12, which is what -8 emits, get their own unrolled loops instead of the generic chunked one. An ARMv5E kernel using the packed 16-bit multiplies is included behind FLAC_LPC32_NARROW_ASM, off by default: it needs bps <= 16, and only about half the subframes of such a stream can use it. Measured with test_codec on 24-bit/96kHz streams. At predictor order 12: 36.24 MHz on e200 (ARMv4) against 62.29 MHz before. The same file improves from 37.65 MHz to 27.3 MHz on Clip+ (ARMv5). Bit-exact against flac -d over four complete streams on both targets, with and without the assembly, and the kernels are checked against an int64_t reference across every order 1-32, qlevel 0-15 and coefficient precision 1-15. The rarely-executed orders stay in DRAM: the codec's IRAM window on PP502x is nearly full and demoting them measured no cost, leaving 96 bytes free. FLAC_LPC32_NO_IRAM demotes the whole filter. Co-Authored-By: Claude Opus 5 Change-Id: Icd8bd298973b9f5c26c103af8adf47b03489d69f --- lib/rbcodec/codecs/libffmpegFLAC/arm.S | 538 +++++++++++++++++++++ lib/rbcodec/codecs/libffmpegFLAC/arm.h | 9 + lib/rbcodec/codecs/libffmpegFLAC/decoder.c | 23 +- 3 files changed, 569 insertions(+), 1 deletion(-) diff --git a/lib/rbcodec/codecs/libffmpegFLAC/arm.S b/lib/rbcodec/codecs/libffmpegFLAC/arm.S index 8c4e810da8..ef072b9093 100644 --- a/lib/rbcodec/codecs/libffmpegFLAC/arm.S +++ b/lib/rbcodec/codecs/libffmpegFLAC/arm.S @@ -270,3 +270,541 @@ lpc_decode_arm: .exit: ldmpc regs=r4-r11 + +/* --------------------------------------------------------------------------- + The 64-bit LPC filter. + + void flac_lpc_32_arm(int32_t *decoded, int coeffs[], int pred_order, + int qlevel, int len); + + Same contract as flac_lpc_32_c in decoder.c, which decode_subframe_lpc uses + whenever bps + coeff_prec + av_log2(pred_order) exceeds 32 -- every 24-bit + stream, and any 16-bit stream encoded at high coefficient precision: + + for (i = pred_order; i < len; i++, decoded++) { + int64_t sum = 0; + for (j = 0; j < pred_order; j++) + sum += (int64_t)coeffs[pred_order-j-1] * decoded[j]; + decoded[j] += sum >> qlevel; + } + + The oldest history sample multiplies the highest-numbered coefficient, the + same convention lpc_decode_arm above uses. + + Orders 1..8 get their own loop with every coefficient held in a register: + the C loop reloads all of them for every output sample although they are + invariant for the whole call, and spills the loop bound to the stack on top + of that. Orders above 8 go through a chunked loop; the reference encoder's + default maximum order is 8 and no test stream here exceeds it. + + Registers, all orders: + + r0 history pointer, walks on one sample per output + r1:r2 64-bit accumulator (r1 low); on entry coeffs and pred_order + r3, lr history samples, and scratch in the tail + r12 ((outputs remaining - 1) << 5) | qlevel + r4... coefficients, ascending: r4 = coeffs[0] + ...r11 further coefficients, then spare history samples + + Packing the output counter and qlevel into one register is what makes order + 8 fit: eight coefficients, a 64-bit accumulator, a pointer and the counter + are already thirteen of the fourteen usable registers. Unpacking costs one + AND per output sample and buys a second sample register, which is what keeps + every load two instructions clear of the multiply that consumes it -- worth + a cycle per tap on both cores. + + Multiply operand order is deliberate. On ARM7TDMI the multiplier walks Rs + eight bits at a time and stops once the rest of Rs is all sign, so the + coefficient -- fifteen bits or fewer by construction, since coeff_prec is + capped at 15 -- goes in Rs and the sample in Rm. That is two passes rather + than four whatever the sample width, which is why a 24-bit stream pays no + more per multiply here than a 16-bit one. + */ + +/* Placement. The hot half of this filter wants IRAM on PP502x, but the + codec's 80 KB window there is nearly full before it arrives: FLAC already + keeps four 18 KB sample buffers in it. So the orders that are a fraction + of a percent of any real stream, and the generic loop that covers the rest, + are left in DRAM -- a dense loop like this is cache-resident after its first + pass anyway. Define FLAC_LPC32_NO_IRAM to demote the whole thing on a + target that needs the space back. */ +#if defined(USE_IRAM) && !defined(FLAC_LPC32_NO_IRAM) +#define LPC32_HOT .section .icode,"ax",%progbits +#else +#define LPC32_HOT .text +#endif +#define LPC32_COLD .text + + /* Shift the accumulator down by qlevel, add the residual waiting at + p[order], store it back, and step the window on by one sample. The + loads above leave r0 at p[order], so the store's post-index rewinds it. + A register-specified shift by 32 yields zero on ARM, which is exactly + what the qlevel == 0 case needs from the high word. */ + .macro LPCTAIL order + ldr r3, [r0] @ residual + and lr, r12, #31 @ qlevel + mov r1, r1, lsr lr + rsb lr, lr, #32 + orr r1, r1, r2, lsl lr + add r1, r3, r1 + str r1, [r0], #-(4 * \order - 4) + subs r12, r12, #32 @ one output done; the borrow out of the + .endm @ count field is the loop's exit test + + /* The high-order tail. Orders above 8 cannot hold their coefficients, so + those loops keep the accumulator in r10:r11 and have r2 free for a + fourth history sample -- the order is a constant there, so nothing + needs to remember it. */ + .macro HTAIL order + ldr r2, [r0] @ residual + and lr, r12, #31 @ qlevel + mov r10, r10, lsr lr + rsb lr, lr, #32 + orr r10, r10, r11, lsl lr + add r10, r2, r10 + str r10, [r0], #-(4 * \order - 4) + subs r12, r12, #32 + .endm + + /* One pass of four taps, for the orders that cannot keep their + coefficients in registers. Both operand streams burst: coefficients + descend from the top, history ascends. r2 is the fourth sample + register, which is what lets the samples come in one ldm of four + rather than three plus a single load. */ + .macro HCHUNK first + ldmdb r3!, { r4-r7 } + ldmia r0!, { r2, r8, r9, lr } + .ifnb \first + smull r10, r11, r2, r7 @ opens the accumulator, so it never + .else @ has to be zeroed + smlal r10, r11, r2, r7 + .endif + smlal r10, r11, r8, r6 + smlal r10, r11, r9, r5 + smlal r10, r11, lr, r4 + .endm + + LPC32_HOT + .align 2 + .global flac_lpc_32_arm +flac_lpc_32_arm: + stmfd sp!, { r4-r11, lr } + ldr r12, [sp, #36] @ len + subs r12, r12, r2 @ outputs = len - pred_order + ble .Lx_ret + sub r12, r12, #1 + orr r12, r3, r12, lsl #5 +.Lx_entry: @ internal: r0, r1, r2 and r12 set up + cmp r2, #12 + addls pc, pc, r2, lsl #2 + ldr pc, .Lx_pgen @ 13..32: the generic loop, in DRAM +@ jumptable: + b .Lx_ret @ order 0 cannot occur + ldr pc, .Lx_p1 @ 1..3 are a fraction of a percent of + ldr pc, .Lx_p2 @ any stream and the tail dominates them + ldr pc, .Lx_p3 @ anyway, so they sit in DRAM + b .Lx_order4 + b .Lx_order5 + b .Lx_order6 + b .Lx_order7 + b .Lx_order8 + ldr pc, .Lx_pgen @ 9 -- generic loop + b .Lx_order10 + ldr pc, .Lx_pgen @ 11 -- generic loop +@ order 12 is the last table slot, so it falls through. 10 and 12 get their +@ own loops because they are what the reference encoder's -8 actually emits; +@ 9 and 11 would too if there were IRAM to put them in. +.Lx_order12: + add r3, r1, #48 @ coefficient pointer, descending +.Lx_loop12: + HCHUNK first + HCHUNK + HCHUNK + HTAIL 12 + bpl .Lx_order12 + ldmpc regs=r4-r11 + +.Lx_order10: + add r3, r1, #40 +.Lx_loop10: + ldmdb r3!, { r4, r5 } @ coeffs[8..9], the two odd taps + ldmia r0!, { r2, r8 } @ p[0..1] + smull r10, r11, r2, r5 + smlal r10, r11, r8, r4 + HCHUNK + HCHUNK + HTAIL 10 + bpl .Lx_order10 + ldmpc regs=r4-r11 +@ order 8 is reached from the table above +.Lx_order8: + ldmia r1, { r4-r11 } @ coeffs[0..7], resident for the call +.Lx_loop8: + ldmia r0!, { r3, lr } @ p[0..1]. Eight resident coefficients + smull r1, r2, r3, r11 @ leave room for two samples, and a pair + smlal r1, r2, lr, r10 @ is still worth bursting: an ldm of n is + ldmia r0!, { r3, lr } @ n+1 cycles against 2n for separate + smlal r1, r2, r3, r9 @ loads, so two words cost 3 rather than + smlal r1, r2, lr, r8 @ 4. The transfer writes r3 before lr, + ldmia r0!, { r3, lr } @ so consuming r3 first both avoids the + smlal r1, r2, r3, r7 @ stall and earns back the final I cycle. + smlal r1, r2, lr, r6 + ldmia r0!, { r3, lr } @ p[6..7] + smlal r1, r2, r3, r5 + smlal r1, r2, lr, r4 + LPCTAIL 8 + bpl .Lx_loop8 + ldmpc regs=r4-r11 + +.Lx_order7: + ldmia r1, { r4-r10 } @ coeffs[0..6] +.Lx_loop7: + ldmia r0!, { r3, r11, lr } @ p[0..2]: 3+2 cycles against 3x2 for + smull r1, r2, r3, r10 @ single loads on ARM7TDMI + smlal r1, r2, r11, r9 + smlal r1, r2, lr, r8 + ldmia r0!, { r3, r11, lr } @ p[3..5] + smlal r1, r2, r3, r7 @ r3 lands first, lr last, so reading r3 + smlal r1, r2, r11, r6 @ straight after the ldm cannot stall + ldr r3, [r0], #4 @ p[6] + smlal r1, r2, lr, r5 + smlal r1, r2, r3, r4 + LPCTAIL 7 + bpl .Lx_loop7 + ldmpc regs=r4-r11 + +.Lx_order6: + ldmia r1, { r4-r9 } @ coeffs[0..5] +.Lx_loop6: + ldmia r0!, { r3, r10, r11, lr } @ p[0..3]: 4+2 against 4x2 + smull r1, r2, r3, r9 + smlal r1, r2, r10, r8 + smlal r1, r2, r11, r7 + ldmia r0!, { r3, r10 } @ p[4..5] + smlal r1, r2, lr, r6 @ lr is still the first ldm's + smlal r1, r2, r3, r5 + smlal r1, r2, r10, r4 + LPCTAIL 6 + bpl .Lx_loop6 + ldmpc regs=r4-r11 + +.Lx_order5: + ldmia r1, { r4-r8 } @ coeffs[0..4] +.Lx_loop5: + ldmia r0!, { r3, r9, r10, r11, lr } @ the whole window in 5+2 cycles + smull r1, r2, r3, r8 + smlal r1, r2, r9, r7 + smlal r1, r2, r10, r6 + smlal r1, r2, r11, r5 + smlal r1, r2, lr, r4 + LPCTAIL 5 + bpl .Lx_loop5 + ldmpc regs=r4-r11 + +.Lx_order4: + ldmia r1, { r4-r7 } @ coeffs[0..3] +.Lx_loop4: + ldmia r0!, { r3, r8, r9, lr } + smull r1, r2, r3, r7 + smlal r1, r2, r8, r6 + smlal r1, r2, r9, r5 + smlal r1, r2, lr, r4 + LPCTAIL 4 + bpl .Lx_loop4 + ldmpc regs=r4-r11 + + LPC32_COLD + .align 2 +.Lx_order3: + ldmia r1, { r4-r6 } @ coeffs[0..2] +.Lx_loop3: + ldmia r0!, { r3, r8, lr } + smull r1, r2, r3, r6 + smlal r1, r2, r8, r5 + smlal r1, r2, lr, r4 + LPCTAIL 3 + bpl .Lx_loop3 + ldmpc regs=r4-r11 + +.Lx_order2: + ldmia r1, { r4-r5 } @ coeffs[0..1] +.Lx_loop2: + ldmia r0!, { r3, lr } + smull r1, r2, r3, r5 + smlal r1, r2, lr, r4 + LPCTAIL 2 + bpl .Lx_loop2 + ldmpc regs=r4-r11 + + /* Order 1 carries the sample it just reconstructed into the next + iteration in r3: it is the only history the next output needs, so the + loop holds one load and one multiply and never stalls. */ +.Lx_order1: + ldr r4, [r1] @ the one coefficient + ldr r3, [r0], #4 @ p[0] +.Lx_loop1: + smull r1, r2, r3, r4 + ldr r3, [r0] @ residual + and lr, r12, #31 + mov r1, r1, lsr lr + rsb lr, lr, #32 + orr r1, r1, r2, lsl lr + add r3, r3, r1 @ and this is the next p[0] + str r3, [r0], #4 + subs r12, r12, #32 + bpl .Lx_loop1 + ldmpc regs=r4-r11 + + /* Orders 9..32. Four taps a pass, both operand streams bursted: the + coefficients descend from coeffs + order and the history ascends, so the + inner loop ends when the coefficient pointer reaches coeffs and needs no + counter of its own. order & 3 leftover taps go first, one at a time. */ +.Lx_highgen: @ orders 9, 11 and 13..32 +.Lx_dnext: + add r3, r1, r2, lsl #2 @ coefficient pointer, descending + mov r10, #0 @ 64-bit accumulator (r10 low) + mov r11, #0 + ands lr, r2, #3 + beq .Lx_dchunk +.Lx_drem: + ldr r8, [r0], #4 + ldr r9, [r3, #-4]! + subs lr, lr, #1 + smlal r10, r11, r8, r9 + bne .Lx_drem +.Lx_dchunk: + ldmdb r3!, { r4-r7 } @ four coefficients, descending + ldmia r0!, { r8, r9, lr } @ three samples, and the fourth below, + smlal r10, r11, r8, r7 @ because r8 is free again by then + ldr r8, [r0], #4 + smlal r10, r11, r9, r6 + smlal r10, r11, lr, r5 + smlal r10, r11, r8, r4 + cmp r3, r1 + bhi .Lx_dchunk + ldr r3, [r0] @ residual, r0 is at p[order] + and lr, r12, #31 + mov r10, r10, lsr lr + rsb lr, lr, #32 + orr r10, r10, r11, lsl lr + add r10, r3, r10 + str r10, [r0] + sub r0, r0, r2, lsl #2 @ back to p[0]... + add r0, r0, #4 @ ...and on one sample + subs r12, r12, #32 + bpl .Lx_dnext + ldmpc regs=r4-r11 + + LPC32_HOT +.Lx_ret: + ldmpc regs=r4-r11 +.Lx_p1: .word .Lx_order1 +.Lx_p2: .word .Lx_order2 +.Lx_p3: .word .Lx_order3 +.Lx_pgen: .word .Lx_highgen + +#if ARM_ARCH >= 5 +/* --------------------------------------------------------------------------- + The same filter for ARMv5E, using the packed 16-bit multiplies. + + void flac_lpc_32_arm_narrow(int32_t *decoded, int coeffs[], int pred_order, + int qlevel, int len); + + Only valid where every history sample fits a signed halfword, which is what + bps <= 16 guarantees on entry: the warm-up samples come from get_sbits(bps), + and every sample this loop reconstructs is checked before the next output + reads it. The caller selects it on bps; it falls back to the 32-bit kernel + above by itself whenever a precondition fails, so it is never the reason an + answer differs. + + Why this needs its own kernel, and why ARMv4 gets none: + + smlal is 32x32 -> 64 and costs three cycles on ARM926. smlabb is + 16x16 + 32 -> 32 and costs one. Two things have to hold before the cheap + one is exact. The sample must fit sixteen bits -- true only for bps <= 16, + which is why a 24-bit stream cannot use this and stays on smlal. And the + sum must fit 32 bits, which holds iff sum|coeffs| * 32768 < 2^31, i.e. + sum|coeffs| <= 65535. That is a property of the subframe's coefficients, + so it is tested once per call, not per sample. Measured over the test + corpus it holds for 94% of the 16-bit high-precision subframes. + + Splitting a wider sample into halves was measured and rejected: two + smlalbb plus the extraction is five cycles against smlal's three, so for + bps > 16 the 32-bit multiply is not merely adequate, it is optimal. + + Registers: + + r0 history pointer r1 coeffs, kept for the fallback + r2 32-bit accumulator r3 scratch + r4..r7 coefficients, packed two to a register, low half first + r8..r11, lr history samples r12 packed count and qlevel + */ + + /* Pack coeffs[2i] and coeffs[2i+1] into one register, low index in the + low half, so that smlabb selects the even coefficient and smlabt the + odd one. Three instructions once per call, not per sample. */ + .macro PACK2 dst, even, odd + mov \dst, \even, lsl #16 + mov \dst, \dst, lsr #16 + orr \dst, \dst, \odd, lsl #16 + .endm + + /* Accumulator down by qlevel, plus the residual. One register-specified + shift folded into the add, where the 64-bit kernel needs a shift, a + reverse subtract and an or. */ + .macro NTAIL order + ldr r3, [r0] @ residual + and lr, r12, #31 @ qlevel + add r2, r3, r2, asr lr + str r2, [r0], #-(4 * \order - 4) + .endm + + /* Does the sample just reconstructed still fit a signed halfword? If not, + the remaining outputs go through the 32-bit kernel. Three cycles per + output sample, against the eight per output that the cheap multiplies + save at order 8. */ + .macro NGUARD order + add r3, r2, #0x8000 + cmp r3, #0x10000 + bcs .Ln_bail\order + .endm + + .macro NBAIL order +.Ln_bail\order: + subs r12, r12, #32 @ that output was finished + ldmpc cond=mi, regs=r4-r11 @ and it was the last one + mov r2, #\order + b .Lx_entry + .endm + + .align 2 + .global flac_lpc_32_arm_narrow +flac_lpc_32_arm_narrow: + stmfd sp!, { r4-r11, lr } + ldr r12, [sp, #36] @ len + subs r12, r12, r2 @ outputs = len - pred_order + ble .Ln_ret + sub r12, r12, #1 + orr r12, r3, r12, lsl #5 + @ sum|coeffs| <= 65535, or the 32-bit accumulator could overflow + mov r3, #0 + sub lr, r2, #1 +.Ln_sum: + ldr r4, [r1, lr, lsl #2] @ walks coeffs[order-1] down to coeffs[0] + cmp r4, #0 + rsblt r4, r4, #0 + add r3, r3, r4 + subs lr, lr, #1 + bge .Ln_sum + cmp r3, #0x10000 + bcs .Lx_entry @ too large: use the 32-bit kernel + cmp r2, #8 + addls pc, pc, r2, lsl #2 + b .Lx_entry @ orders 9 and up: 32-bit kernel +@ jumptable: + b .Ln_ret @ order 0 cannot occur + b .Lx_entry @ orders 1..4 are left to the 32-bit + b .Lx_entry @ kernel: they are absent from every + b .Lx_entry @ stream that reaches this path, and the + b .Lx_entry @ tail dominates them anyway + b .Ln_order5 + b .Ln_order6 + b .Ln_order7 +@ order 8 falls through +.Ln_order8: + ldmia r1, { r8, r9, r10, r11 } @ coeffs[0..3] + PACK2 r4, r8, r9 + PACK2 r5, r10, r11 + add lr, r1, #16 + ldmia lr, { r8, r9, r10, r11 } @ coeffs[4..7] + PACK2 r6, r8, r9 + PACK2 r7, r10, r11 +.Ln_loop8: + ldmia r0!, { r8, r9, r10, r11 } @ p[0..3] + smulbt r2, r8, r7 @ p[0] * coeffs[7]: one cycle, and it + smlabb r2, r9, r7, r2 @ needs no accumulator to be zeroed + smlabt r2, r10, r6, r2 + smlabb r2, r11, r6, r2 + ldmia r0!, { r8, r9, r10, r11 } @ p[4..7] + smlabt r2, r8, r5, r2 + smlabb r2, r9, r5, r2 + smlabt r2, r10, r4, r2 + smlabb r2, r11, r4, r2 + NTAIL 8 + NGUARD 8 + subs r12, r12, #32 + bpl .Ln_loop8 + ldmpc regs=r4-r11 + NBAIL 8 + +.Ln_order7: + ldmia r1, { r8, r9, r10, r11 } @ coeffs[0..3] + PACK2 r4, r8, r9 + PACK2 r5, r10, r11 + add lr, r1, #16 + ldmia lr, { r8, r9, r10 } @ coeffs[4..6] + PACK2 r6, r8, r9 + mov r7, r10 @ coeffs[6] on its own, low half +.Ln_loop7: + ldmia r0!, { r8, r9, r10, r11 } @ p[0..3] + smulbb r2, r8, r7 @ p[0] * coeffs[6] + smlabt r2, r9, r6, r2 + smlabb r2, r10, r6, r2 + smlabt r2, r11, r5, r2 + ldmia r0!, { r8, r9, r10 } @ p[4..6] + smlabb r2, r8, r5, r2 + smlabt r2, r9, r4, r2 + smlabb r2, r10, r4, r2 + NTAIL 7 + NGUARD 7 + subs r12, r12, #32 + bpl .Ln_loop7 + ldmpc regs=r4-r11 + NBAIL 7 + +.Ln_order6: + ldmia r1, { r8, r9, r10, r11 } @ coeffs[0..3] + PACK2 r4, r8, r9 + PACK2 r5, r10, r11 + add lr, r1, #16 + ldmia lr, { r8, r9 } @ coeffs[4..5] + PACK2 r6, r8, r9 +.Ln_loop6: + ldmia r0!, { r8, r9, r10, r11 } @ p[0..3] + smulbt r2, r8, r6 @ p[0] * coeffs[5] + smlabb r2, r9, r6, r2 + smlabt r2, r10, r5, r2 + smlabb r2, r11, r5, r2 + ldmia r0!, { r8, r9 } @ p[4..5] + smlabt r2, r8, r4, r2 + smlabb r2, r9, r4, r2 + NTAIL 6 + NGUARD 6 + subs r12, r12, #32 + bpl .Ln_loop6 + ldmpc regs=r4-r11 + NBAIL 6 + +.Ln_order5: + ldmia r1, { r8, r9, r10, r11 } @ coeffs[0..3] + PACK2 r4, r8, r9 + PACK2 r5, r10, r11 + ldr r6, [r1, #16] @ coeffs[4] on its own, low half +.Ln_loop5: + ldmia r0!, { r8, r9, r10, r11 } @ p[0..3] + ldr lr, [r0], #4 @ p[4], loaded well clear of its multiply + smulbb r2, r8, r6 @ p[0] * coeffs[4] + smlabt r2, r9, r5, r2 + smlabb r2, r10, r5, r2 + smlabt r2, r11, r4, r2 + smlabb r2, lr, r4, r2 + NTAIL 5 + NGUARD 5 + subs r12, r12, #32 + bpl .Ln_loop5 + ldmpc regs=r4-r11 + NBAIL 5 + +.Ln_ret: + ldmpc regs=r4-r11 +#endif /* ARM_ARCH >= 5 */ diff --git a/lib/rbcodec/codecs/libffmpegFLAC/arm.h b/lib/rbcodec/codecs/libffmpegFLAC/arm.h index 39080d7f75..df3bae5611 100644 --- a/lib/rbcodec/codecs/libffmpegFLAC/arm.h +++ b/lib/rbcodec/codecs/libffmpegFLAC/arm.h @@ -5,4 +5,13 @@ void lpc_decode_arm(int blocksize, int qlevel, int pred_order, int32_t* data, int* coeffs); +/* The 64-bit LPC filter, the wide path of decode_subframe_lpc. Both have the + same contract as flac_lpc_32_c in decoder.c. The _narrow form uses the + ARMv5E packed 16-bit multiplies and is only valid when every history sample + fits a signed halfword, which bps <= 16 guarantees. */ +void flac_lpc_32_arm(int32_t *decoded, int coeffs[], int pred_order, + int qlevel, int len); +void flac_lpc_32_arm_narrow(int32_t *decoded, int coeffs[], int pred_order, + int qlevel, int len); + #endif diff --git a/lib/rbcodec/codecs/libffmpegFLAC/decoder.c b/lib/rbcodec/codecs/libffmpegFLAC/decoder.c index 9701a598b2..ef81a89d8b 100644 --- a/lib/rbcodec/codecs/libffmpegFLAC/decoder.c +++ b/lib/rbcodec/codecs/libffmpegFLAC/decoder.c @@ -238,8 +238,16 @@ int decode_subframe_fixed(FLACContext *s, int32_t* decoded, int pred_order, int } #if !defined(CPU_COLDFIRE) +/* The assembler kernels in arm.S implement flac_lpc_32_c exactly; keep the C + version compiled anyway when they are in use, so that a build can A/B the + two by defining FLAC_NO_LPC32_ASM. */ +#if defined(CPU_ARM_CLASSIC) && !defined(FLAC_NO_LPC32_ASM) +#define FLAC_LPC32_ASM +#endif + static void flac_lpc_32_c(int32_t *decoded, int coeffs[], - int pred_order, int qlevel, int len) ICODE_ATTR_FLAC; + int pred_order, int qlevel, int len) + ICODE_ATTR_FLAC UNUSED_ATTR; static void flac_lpc_32_c(int32_t *decoded, int coeffs[], int pred_order, int qlevel, int len) { @@ -340,7 +348,20 @@ static int decode_subframe_lpc(FLACContext *s, int32_t* decoded, int pred_order, lpc_decode_emac_wide(s->blocksize - pred_order, qlevel, pred_order, decoded + pred_order, coeffs); #else + #if defined(FLAC_LPC32_ASM) + /* The 16-bit multiply kernel needs every history sample to fit a + signed halfword, which bps <= 16 guarantees, and checks its other + precondition itself. */ + #if (ARM_ARCH >= 5) && defined(FLAC_LPC32_NARROW_ASM) + if (bps <= 16) + flac_lpc_32_arm_narrow(decoded, coeffs, pred_order, qlevel, + s->blocksize); + else + #endif + flac_lpc_32_arm(decoded, coeffs, pred_order, qlevel, s->blocksize); + #else flac_lpc_32_c(decoded, coeffs, pred_order, qlevel, s->blocksize); + #endif if (bps <= 16) lpc_analyze_remodulate(decoded, coeffs, pred_order, qlevel, s->blocksize, bps); From 279e8524a63483ac3647ddf4dbd34c5ed9cc50db Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Mon, 5 Oct 2026 14:09:38 -0400 Subject: [PATCH 353/408] makezip: Don't install classic_statusbar.rsbs on targets without remotes Also nuke attempting to install the nonexistent 'rockbox_none.sbs' Change-Id: I22ed33c5d3d322b0509118005b1327aa88c61fcc --- tools/buildzip.pl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tools/buildzip.pl b/tools/buildzip.pl index 99fb04cad2..38b7c870b9 100755 --- a/tools/buildzip.pl +++ b/tools/buildzip.pl @@ -668,12 +668,11 @@ sub buildzip { } else { copy("$ROOT/wps/classic_statusbar.mono.sbs", "$temp_dir/wps/classic_statusbar.sbs"); } - if ($remote_depth != $depth) { - copy("$ROOT/wps/classic_statusbar.mono.sbs", "$temp_dir/wps/classic_statusbar.rsbs"); - } else { + if ($remote_depth == $depth) { copy("$temp_dir/wps/classic_statusbar.sbs", "$temp_dir/wps/classic_statusbar.rsbs"); + } elsif ($remote_depth != 0) { + copy("$ROOT/wps/classic_statusbar.mono.sbs", "$temp_dir/wps/classic_statusbar.rsbs"); } - copy("$temp_dir/wps/rockbox_none.sbs", "$temp_dir/wps/rockbox_none.rsbs"); # and the info file copy("rockbox-info.txt", "$temp_dir/rockbox-info.txt"); From 59f15ca63079f0bd735d0b02d686257bd7b5205b Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Mon, 5 Oct 2026 14:08:37 -0400 Subject: [PATCH 354/408] cabbiev2: Don't generate certain entries for monochrome devices Namely, 'backdrop' and 'filetype colours' Change-Id: Ied0b87d9b43d010a37ebb1e33488463c6ad1ebcc --- wps/wpsbuild.pl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/wps/wpsbuild.pl b/wps/wpsbuild.pl index 41facaf35e..7491224039 100755 --- a/wps/wpsbuild.pl +++ b/wps/wpsbuild.pl @@ -290,10 +290,13 @@ MOO } push @out, "selector type: $selecttype\n" if (defined($selecttype)); - push @out, "backdrop: $backdrop\n" if (defined($backdrop)); - push @out, "filetype colours: $filetylecolor\n" if (defined($filetylecolor)); + + if ($main_depth > 1) { + push @out, "backdrop: $backdrop\n" if (defined($backdrop)); + } if ($main_depth > 2) { + push @out, "filetype colours: $filetylecolor\n" if (defined($filetylecolor)); push @out, "foreground color: $fgcolor\n" if($fgcolor); push @out, "background color: $bgcolor\n" if($bgcolor); push @out, "line selector start color: $lineselectstart\n" if($lineselectstart); @@ -407,7 +410,7 @@ $wpsdir = $1; open(WPS, "<$wpslist"); while() { my $l = $_; - + # remove CR $l =~ s/\r//g; if($l =~ /^ *\#/) { From ce968e7da1f53239809f409ba6092b0269d44d54 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Mon, 5 Oct 2026 22:30:01 +0200 Subject: [PATCH 355/408] rk27xx: don't reprogram the codec PLL for an unchanged rate Every start of recording applies the sample rate again, mostly unchanged, and set_codec_freq() reprogrammed the codec PLL each time. That glitches MCLK and every I2S clock made of it, right before the recording receiver is reset and starts on them. Whether it then kept its framing came down to code timing: on the Samsung YP-CP3 the recording screen's peak meter showed one channel empty and the other saturated on some visits, and builds with debug code added never did. The PLL is now left alone when it already runs the rate asked for. On a real change the lock bit, which may still show the old lock at first, is polled only after the 0.3 ms the datasheet gives for locking, with a timeout of at least a full tick, and the clocks get 1 ms more to settle before anything starts on them. Tested on a YP-CP3, together with the codec as I2S master: the peak meter on every one of many visits to the recording screen, with the microphone and FM, and at 22 kHz. Co-Authored-By: Claude Opus 5.5 Change-Id: I2267e17c5bc7d8044715e762dbb6b92f45af4dcc --- firmware/target/arm/rk27xx/pcm-rk27xx.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/firmware/target/arm/rk27xx/pcm-rk27xx.c b/firmware/target/arm/rk27xx/pcm-rk27xx.c index f37eb194b0..2aa5979ce8 100644 --- a/firmware/target/arm/rk27xx/pcm-rk27xx.c +++ b/firmware/target/arm/rk27xx/pcm-rk27xx.c @@ -207,8 +207,17 @@ static void i2s_init(void) */ static void set_codec_freq(unsigned int freq) { + static unsigned int cur_freq = HW_NUM_FREQ; /* none yet */ long timeout; + /* Reprogramming the PLL glitches MCLK, and with it every clock the + * codec makes of it: an I2S side started on them can lose its framing. + * The rate is applied again on every start of recording, mostly + * unchanged. */ + if (freq == cur_freq) + return; + cur_freq = freq; + /* {CLKR, CLKF, CLKOD, CODECPLL_DIV} */ static const unsigned int pcm_freq_params[HW_NUM_FREQ][4] = { @@ -237,13 +246,18 @@ static void set_codec_freq(unsigned int freq) (pcm_freq_params[freq][2]<<1) ; /* CLKOD factor */ /* wait for CODEC PLL lock with 10 ms timeout - * datasheet states that pll lock should take approx. 0.3 ms + * datasheet states that pll lock should take approx. 0.3 ms; the lock bit + * may still show the old lock at first */ - timeout = current_tick + (HZ/100); + udelay(300); + timeout = current_tick + (HZ/100) + 1; while (!(SCU_STATUS & (1<<2))) if (TIME_AFTER(current_tick, timeout)) break; + /* let the codec's clocks settle before an I2S side starts on them */ + udelay(1000); + } #endif From 361913fdd247bd1bc42e4bb5827eb179af3232b0 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Mon, 5 Oct 2026 22:30:01 +0200 Subject: [PATCH 356/408] YP-CP3: make the WM8750 the I2S master The WM8750 frames its ADC on ADCLRC and its DAC on DACLRC. As the I2S slave both are inputs, and the rk27xx has a single LRCK, which on the YP-CP3 evidently does not reach ADCLRC: with the rk27xx as the master, the recording DMA got exact zeros on most visits to the recording screen and real samples only by chance, sometimes changing partway through a visit. That stays so with the codec PLL no longer glitching the I2S clocks (previous commit); with the codec as the master it does not happen. The codec now drives BCLK and both LRCKs itself, and the rk27xx I2S transmitter and receiver are slaves. The codec's MCLK still comes from the rk27xx codec PLL at 256 fs, so sample rates stay exact. The original firmware runs the codec as master too, but off a fixed 12 MHz in USB mode. RK27XX_I2S_MCLK says the rk27xx makes the codec's MCLK, apart from CODEC_SLAVE, which also makes it the I2S master. The YP-CP3 drops CODEC_SLAVE for RK27XX_I2S_MCLK, and the WM8750 driver sets its master bit as it does for any codec that is not a slave. Tested on a YP-CP3: playback, FM radio, the recording screen's peak meter on every visit, recording from the microphone and from FM. Co-Authored-By: Claude Opus 5.5 Change-Id: I0dc93d3b2c45fddf61649702a931d11806e78cbf --- firmware/export/config/samsungypcp3.h | 12 +++++++----- firmware/target/arm/rk27xx/pcm-rk27xx.c | 7 ++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/firmware/export/config/samsungypcp3.h b/firmware/export/config/samsungypcp3.h index 658b327165..f3542f64f6 100644 --- a/firmware/export/config/samsungypcp3.h +++ b/firmware/export/config/samsungypcp3.h @@ -94,13 +94,15 @@ #define CONFIG_LCD LCD_SPFD5420A /* Wolfson WM8750 on I2C, headphones on its OUT2. The codec is the I2S - * slave: the rk27xx drives the bus and clocks the codec from its codec PLL - * at 256 fs for every rate (pcm-rk27xx.c), so the codec's CLOCKING register - * is its normal-mode 256 fs setting throughout. The original firmware runs - * the codec as master off a fixed 12 MHz instead, in USB mode, which puts + * master, driving BCLK and both of its LRCKs: as a slave it frames the ADC + * on its ADCLRC input, which the rk27xx's one LRCK does not reach, and + * records zeros. Its MCLK comes from the rk27xx's codec PLL at 256 fs for + * every rate (pcm-rk27xx.c), so the codec's CLOCKING register is its + * normal-mode 256 fs setting throughout. The original firmware runs the + * codec as master off a fixed 12 MHz instead, in USB mode, which puts * 44.1 kHz at 44.118. */ #define HAVE_WM8750 -#define CODEC_SLAVE +#define RK27XX_I2S_MCLK #define CODEC_SRCTRL_11025HZ 0 #define CODEC_SRCTRL_22050HZ 0 #define CODEC_SRCTRL_44100HZ 0 diff --git a/firmware/target/arm/rk27xx/pcm-rk27xx.c b/firmware/target/arm/rk27xx/pcm-rk27xx.c index 2aa5979ce8..2040663a76 100644 --- a/firmware/target/arm/rk27xx/pcm-rk27xx.c +++ b/firmware/target/arm/rk27xx/pcm-rk27xx.c @@ -196,8 +196,9 @@ static void i2s_init(void) #endif } -#ifdef CODEC_SLAVE -/* When codec is slave we need to setup i2s MCLK clock using codec pll. +#if defined(CODEC_SLAVE) || defined(RK27XX_I2S_MCLK) +/* When codec is slave, or master off our MCLK (RK27XX_I2S_MCLK), we need + * to setup i2s MCLK clock using codec pll. * The MCLK frequency is 256*codec frequency as i2s setup is: * LRCK/SCLK = 64 and MCLK/SCLK = 4 (see i2s_init() for reference) * @@ -278,7 +279,7 @@ static void sink_dma_init(void) static void sink_set_freq(uint16_t freq) { -#ifdef CODEC_SLAVE +#if defined(CODEC_SLAVE) || defined(RK27XX_I2S_MCLK) set_codec_freq(freq); #endif From 4502948cd85170109303207d28d47fff0475e7ce Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 5 Oct 2026 14:36:12 -0400 Subject: [PATCH 357/408] wmapro: add a two-channel path to the channel transform inverse_channel_transform() ran its general N-channel matrix loop for every sample of a stereo stream, where the matrix is exactly +-1.0. That loop was a quarter to a third of the whole decode, and GCC 9.5.0 compiles it worse than 4.9.4 did, which made the codec 6-9% slower on ARM7TDMI after the toolchain update. Handle a group of two channels separately: add and subtract when the matrix is +-1.0, and a plain four multiply loop otherwise. More than two channels still use the general loop. This reverses the regression from the GCC 9.5.0 update and goes well past it. The loop the newer compiler handled badly is no longer used for stereo, so the two compilers now give the same speed to within 1%, about 25% faster than the codec was with GCC 4.9.4 (estimated with perfsim, e200v1, wmapro_141k: 25.81 MHz with 4.9.4 before this change, 19.5 MHz with either compiler after it). Output is bit-identical: whole-file PCM hashes match before and after for five stereo files at 55-271 kbps, built with GCC 9.5.0 and with 4.9.4, and also with the multiply path forced on. Measured with test_codec, wmapro_141k.wma, MHz for real time: Sansa e200v1 27.99 -> 19.70 Sansa Clip+ 21.78 -> 15.80 Estimated with perfsim for the other files (e200v1 / Clip+): wmapro_55k 25.21 -> 17.06 / 20.02 -> 13.71 wmapro_80k 26.17 -> 18.01 / 20.75 -> 14.44 wmapro_173k 28.52 -> 20.29 / 22.55 -> 16.25 wmapro_271k 30.85 -> 22.52 / 24.34 -> 18.04 Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libwmapro/wmaprodec.c | 69 +++++++++++++++++++----- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/lib/rbcodec/codecs/libwmapro/wmaprodec.c b/lib/rbcodec/codecs/libwmapro/wmaprodec.c index 578b8ddb97..09dea96504 100644 --- a/lib/rbcodec/codecs/libwmapro/wmaprodec.c +++ b/lib/rbcodec/codecs/libwmapro/wmaprodec.c @@ -1030,6 +1030,38 @@ static int decode_scale_factors(WMAProDecodeCtx* s) return 0; } +/** + *@brief Apply a 2x2 decorrelation matrix to one band of two channels. + * Two channels are by far the common case; the results are the + * same as the general loop's in inverse_channel_transform(). + *@param ch0 first channel's coefficients + *@param ch1 second channel's coefficients + *@param mat the matrix, as 16.16 fixed point + *@param len number of coefficients + */ +static inline void decorrelate_stereo(int32_t *ch0, int32_t *ch1, + const int32_t *mat, int len) +{ + const int32_t m0 = mat[0], m1 = mat[1], m2 = mat[2], m3 = mat[3]; + + if (m0 == ONE_FRACT16 && m1 == -ONE_FRACT16 && + m2 == ONE_FRACT16 && m3 == ONE_FRACT16) { + /* The matrix of a stereo stream. Multiplying by +-1.0 is exact, + * so add and subtract; unsigned, as the multiply's result wraps. */ + for (; len > 0; len--) { + uint32_t a = *ch0, b = *ch1; + *ch0++ = a - b; + *ch1++ = a + b; + } + } else { + for (; len > 0; len--) { + int32_t a = *ch0, b = *ch1; + *ch0++ = fixmul16(m0, a) + fixmul16(m1, b); + *ch1++ = fixmul16(m2, a) + fixmul16(m3, b); + } + } +} + /** *@brief Reconstruct the individual channel data. *@param s codec context @@ -1052,24 +1084,33 @@ static void inverse_channel_transform(WMAProDecodeCtx *s) sfb < s->cur_sfb_offsets + s->num_bands; sfb++) { int y; if (*tb++ == 1) { - /** multiply values with the decorrelation_matrix */ - for (y = sfb[0]; y < FFMIN(sfb[1], s->subframe_len); y++) { - const int32_t* mat = s->chgroup[i].fixdecorrelation_matrix; - const int32_t* data_end = data + num_channels; - int32_t* data_ptr = data; - int32_t** ch; + if (num_channels == 2) { + decorrelate_stereo(ch_data[0] + sfb[0], + ch_data[1] + sfb[0], + s->chgroup[i].fixdecorrelation_matrix, + FFMIN(sfb[1], s->subframe_len) - sfb[0]); + } else { + /** multiply values with the decorrelation_matrix */ + for (y = sfb[0]; + y < FFMIN(sfb[1], s->subframe_len); y++) { + const int32_t* mat = + s->chgroup[i].fixdecorrelation_matrix; + const int32_t* data_end = data + num_channels; + int32_t* data_ptr = data; + int32_t** ch; - for (ch = ch_data; ch < ch_end; ch++) - *data_ptr++ = (*ch)[y]; + for (ch = ch_data; ch < ch_end; ch++) + *data_ptr++ = (*ch)[y]; - for (ch = ch_data; ch < ch_end; ch++) { - int32_t sum = 0; - data_ptr = data; + for (ch = ch_data; ch < ch_end; ch++) { + int32_t sum = 0; + data_ptr = data; - while (data_ptr < data_end) - sum += fixmul16(*mat++, *data_ptr++); + while (data_ptr < data_end) + sum += fixmul16(*mat++, *data_ptr++); - (*ch)[y] = sum; + (*ch)[y] = sum; + } } } } else if (s->num_channels == 2) { From 4b8ea97fa99bbb5f9111cf55fb870380a4e22725 Mon Sep 17 00:00:00 2001 From: neofright Date: Thu, 3 Sep 2026 22:54:54 +0100 Subject: [PATCH 358/408] Support ROCKBOX_DIR in HibyOS hosted bootloader Currently if you want to compile a bootloader like so: $ ../tools/configure --rbdir=/.rockbox.aigoerosq --target=aigoerosq --type=b This won't work because the bootloader is hard coded to look for the rockbox Linux binary in /.rockbox/rockbox.erosq regardless of what is specified by '--rbdir'. This commit adds support for ROCKBOX_DIR in the hosted HibyOS bootloaders. After compiling, they can be installed with adb via (for example): adb push build/bootloader.erosq /usr/bin/bootloader.erosq Change-Id: Ic4ceb75107f608beacfd497998a11a5874e87ac1 --- bootloader/hibyos_linux.c | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/bootloader/hibyos_linux.c b/bootloader/hibyos_linux.c index d05259536e..f42070d001 100644 --- a/bootloader/hibyos_linux.c +++ b/bootloader/hibyos_linux.c @@ -36,6 +36,7 @@ #include "lcd.h" #include "font.h" #include "power.h" +#include "rbpaths.h" #include #include #include @@ -143,11 +144,6 @@ #endif #define BASE_DIR PIVOT_ROOT -//#ifdef FIIO_M3K_LINUX -//#define BASE_DIR "/mnt" -//#else -//#define BASE_DIR "/mnt/sd_0" -//#endif /* images */ #include "bitmaps/rockboxicon.h" @@ -258,7 +254,7 @@ static void mount_storage(int enable) static enum boot_mode load_boot_mode(enum boot_mode mode) { mount_storage(true); - int fd = open(BASE_DIR "/.rockbox/rb_bl_mode.txt", O_RDONLY); + int fd = open(BASE_DIR ROCKBOX_DIR "/rb_bl_mode.txt", O_RDONLY); if(fd >= 0) { read(fd, &mode, sizeof(mode)); @@ -270,7 +266,7 @@ static enum boot_mode load_boot_mode(enum boot_mode mode) static void save_boot_mode(enum boot_mode mode) { mount_storage(true); - int fd = open(BASE_DIR "/.rockbox/rb_bl_mode.txt", O_RDWR | O_CREAT | O_TRUNC); + int fd = open(BASE_DIR ROCKBOX_DIR "/rb_bl_mode.txt", O_RDWR | O_CREAT | O_TRUNC); if(fd >= 0) { write(fd, &mode, sizeof(mode)); @@ -720,14 +716,14 @@ int main(int argc, char **argv) #endif fflush(stdout); mount_storage(true); - system("/bin/cp " BASE_DIR "/.rockbox/" BOOTFILE " /tmp"); + system("/bin/cp " BASE_DIR ROCKBOX_DIR "/" BOOTFILE " /tmp"); system("/bin/chmod +x /tmp/" BOOTFILE); #ifdef HIBY_R1 /* Load libasound from Rockbox directory */ char *argvr1[] = { "/tmp/" BOOTFILE, 0 }; char *envpr1[] = { - "LD_LIBRARY_PATH=/data/mnt/sd_0/.rockbox", + "LD_LIBRARY_PATH=" BASE_DIR "/" ROCKBOX_DIR, 0 }; execve(argvr1[0], &argvr1[0], envpr1); From 6ffbee954b75d68e6e65471d57e7de72b93c6c49 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 5 Oct 2026 20:12:58 -0400 Subject: [PATCH 359/408] wmapro: fix the level and accuracy of 16 bit streams A WMA Professional stream of 16 bits per sample played about 48 dB too quiet and with a noise floor near -70 dBFS. ffmpeg scales the transform's output by the stream's sample size. That was dropped when the decoder was converted to fixed point (d884af2b99, 16284ae8ae), and the output is passed to the DSP as if every stream had 24 bits. A stream of fewer bits has a lower quantization step to match, so it came out low by the difference, and it used the bottom of the integer quantization table, where the factors have only a few significant bits. Decode a 16 or 20 bit stream at the level of a 24 bit stream: use that stream's quantization step, and scale each band's factor by the ratio that is left. 24 bit streams are not affected. Checked with perfsim (Sansa e200v1 build) against ffmpeg's decode of a 16 bit, 192 kbps stereo file, the only such file to hand: level SNR vs ffmpeg noise before -48 dB 37 dB -70 dBFS after correct 85 dB -117 dBFS A 24 bit file's output is byte-identical before and after. The 20 bit case follows the same rule but is untested. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libwmapro/wmaprodec.c | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/rbcodec/codecs/libwmapro/wmaprodec.c b/lib/rbcodec/codecs/libwmapro/wmaprodec.c index 09dea96504..75bdf4d8a1 100644 --- a/lib/rbcodec/codecs/libwmapro/wmaprodec.c +++ b/lib/rbcodec/codecs/libwmapro/wmaprodec.c @@ -224,6 +224,8 @@ typedef struct WMAProDecodeCtx { uint8_t len_prefix; ///< frame is prefixed with its length uint8_t dynamic_range_compression; ///< frame contains DRC data uint8_t bits_per_sample; ///< integer audio sample size for the unscaled IMDCT output (used to scale to [-1.0, 1.0]) + uint8_t quant_step_bias; ///< added to the quantization step of a stream of less than 24 bits + int32_t quant_scale; ///< s1.30 factor for its quantization factors, 0 if none uint16_t samples_per_frame; ///< number of samples to output uint16_t log2_frame_size; int8_t num_channels; ///< number of channels in the stream (same as AVCodecContext.num_channels) @@ -332,6 +334,18 @@ int decode_init(asf_waveformatex_t *wfx) s->decode_flags = AV_RL16(edata_ptr+14); channel_mask = AV_RL32(edata_ptr+2); s->bits_per_sample = AV_RL16(edata_ptr); + + /* A stream of fewer bits has a lower quantization step. Decode it at + * the level of a 24 bit stream instead: use that stream's quantization + * step, and scale the factors by what is left, + * 2^(24-bits) / 10^(bias/20). */ + if (s->bits_per_sample == 16) { + s->quant_step_bias = (90 * 24 >> 4) - (90 * 16 >> 4); + s->quant_scale = 1545752065; /* 256 / 10^(45/20) */ + } else if (s->bits_per_sample == 20) { + s->quant_step_bias = (90 * 24 >> 4) - (90 * 20 >> 4); + s->quant_scale = 1216241597; /* 16 / 10^(23/20) */ + } /** dump the extradata */ for (i = 0; i < wfx->datalen; i++) DEBUGF("[%x] ", wfx->data[i]); @@ -1270,7 +1284,7 @@ static int decode_subframe(WMAProDecodeCtx *s) if (transmit_coeffs) { int step; - int quant_step = 90 * s->bits_per_sample >> 4; + int quant_step = (90 * s->bits_per_sample >> 4) + s->quant_step_bias; /** decode number of vector coded coefficients */ if ((s->transmit_num_vec_coeffs = get_bits1(&s->gb))) { @@ -1371,9 +1385,12 @@ static int decode_subframe(WMAProDecodeCtx *s) DEBUGF("in wmaprodec.c : unhandled value for exp (%d), please report sample.\n", exp); return -1; } - const int32_t quant = QUANT(exp); + int32_t quant = QUANT(exp); int start = s->cur_sfb_offsets[b]; + if (s->quant_scale) + quant = (int64_t)quant * s->quant_scale >> 30; + vector_fixmul_scalar(s->tmp+start, s->channel[c].coeffs + start, quant, end-start); From fb2ee6c08f9c083506fa919ea5b7f0d6b9c13602 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 5 Oct 2026 20:24:39 -0400 Subject: [PATCH 360/408] wma: decode every codec packet in an ASF payload A payload normally holds one codec packet of blockalign bytes, but some files put several in each. Only the first was decoded, so such a file played one packet in every 8 or 15, as a few seconds of broken sound, and then ended. Step through the payload in blockalign sized packets. Checked with perfsim (Sansa e200v1 build) against ffmpeg's decode: doors-test.wma (WMA v2, 11 kHz mono), 15 packets a payload: 80896 of 1205760 samples before, all of them after, 86 dB SNR test.wma (WMA v1, 44.1 kHz stereo), 8 packets a payload: 1294336 of 10346496 samples before, all after, 114 dB SNR The output of 26 other WMA files is byte-identical before and after. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/wma.c | 49 +++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/lib/rbcodec/codecs/wma.c b/lib/rbcodec/codecs/wma.c index 92022f8b90..a8b09ced6b 100644 --- a/lib/rbcodec/codecs/wma.c +++ b/lib/rbcodec/codecs/wma.c @@ -178,29 +178,42 @@ new_packet: goto new_packet; } } else if (res > 0) { - wma_decode_superframe_init(&wmadec, audiobuf, audiobufsize); + /* A payload usually holds one codec packet (a superframe) of + * blockalign bytes, but it can hold several. */ + int packetsize = audiobufsize; + int offset; - for (i=0; i < wmadec.nb_frames; i++) + if (wfx.blockalign > 0 && audiobufsize % wfx.blockalign == 0) + packetsize = wfx.blockalign; + + for (offset = 0; offset < audiobufsize; offset += packetsize) { - wmares = wma_decode_superframe_frame(&wmadec, - audiobuf, audiobufsize); + const uint8_t *packet = audiobuf + offset; - ci->yield (); + wma_decode_superframe_init(&wmadec, packet, packetsize); - if (wmares < 0) { - /* Do the above, but for errors in decode. */ - errcount++; - DEBUGF("WMA decode error %d, errcount %d\n",wmares, errcount); - if (errcount > 5) { - return CODEC_ERROR; - } else { - ci->advance_buffer(packetlength); - goto new_packet; + for (i=0; i < wmadec.nb_frames; i++) + { + wmares = wma_decode_superframe_frame(&wmadec, + packet, packetsize); + + ci->yield (); + + if (wmares < 0) { + /* Do the above, but for errors in decode. */ + errcount++; + DEBUGF("WMA decode error %d, errcount %d\n",wmares, errcount); + if (errcount > 5) { + return CODEC_ERROR; + } else { + ci->advance_buffer(packetlength); + goto new_packet; + } + } else if (wmares > 0) { + ci->pcmbuf_insert((*wmadec.frame_out)[0], (*wmadec.frame_out)[1], wmares); + elapsedtime += (wmares*10)/(wfx.rate/100); + ci->set_elapsed(elapsedtime); } - } else if (wmares > 0) { - ci->pcmbuf_insert((*wmadec.frame_out)[0], (*wmadec.frame_out)[1], wmares); - elapsedtime += (wmares*10)/(wfx.rate/100); - ci->set_elapsed(elapsedtime); } } } From 11b12894ad48205ca77bb79fadf89efba14b0056 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 5 Oct 2026 20:41:06 -0400 Subject: [PATCH 361/408] wma: fix the level of noise coded high bands Low bitrate WMA files code their upper bands as noise at a given power. Those bands came out 12 to 25 dB too quiet, which dulled the treble. Two things were wrong. The exponent pointer was moved too far in blocks whose exponents have another resolution. This is ffmpeg's r20756 (f78501b264, "Fix apparent 10l typos introduced in r8627"); r8627 was merged here in 2f1da8d24a but the fix for it never was. The fixed point gain of a band lost nearly all its precision: it was zero for three bands in four in the file traced, and the sum for a band's power overflowed in a quarter of them. Compute the gain in 64 bits, once a band. The factor for the noise below the first coded coefficient (WMA v1 only) was 16 bits too large and is corrected by the same reasoning; no file here exercises it. Checked with perfsim (Sansa e200v1 build) against ffmpeg's decode, as the mean difference in band levels over the whole file: 01 - Jane Austen (44.1 kHz, 32 kbps) 5.5 dB -> 0.0 dB 07.Devil.In.My.Mind (same) 6.2 dB -> 0.0 dB beyonthepain907z (same) 6.8 dB -> 0.0 dB moshimoashitaga (same) 2.8 dB -> 0.0 dB Four more noise coded files, already within 0.3 dB, now match too. The output of the 15 files without noise coding is byte-identical. Files that use LSP exponents still differ in their top bands; that is a separate fault in the LSP curve. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libwma/wmadeci.c | 106 ++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 22 deletions(-) diff --git a/lib/rbcodec/codecs/libwma/wmadeci.c b/lib/rbcodec/codecs/libwma/wmadeci.c index 56779ca517..a49b057d82 100644 --- a/lib/rbcodec/codecs/libwma/wmadeci.c +++ b/lib/rbcodec/codecs/libwma/wmadeci.c @@ -777,6 +777,69 @@ static int decode_exp_vlc(WMADecodeContext *s, int ch) return 0; } +/* The gain of a noise coded band, as ffmpeg computes it in floating point: + * + * mdct_norm * sqrt(power / last_power) * 10^(value/20) + * / (max_exponent * noise_mult) + * + * returned as the 16.16 factor wma_decode_block() applies to the band. + * 1/noise_mult is 50 or 25. The terms differ too much in size for 32 bits, + * and this runs once a band, so it is all done in 64 bits. */ +static fixed32 noise_band_gain(uint64_t power, uint64_t last_power, int value, + fixed32 max_exponent, fixed32 mdct_norm, + int inv_noise_mult) +{ + uint64_t ratio, root, bit, a, b; + int shift = 16 + 20; + + if (power == 0 || max_exponent <= 0) + return 0; + + /* the power ratio in 16.16, kept small enough to take its root */ + ratio = last_power ? (power << 16) / last_power : (uint64_t)1 << 46; + if (ratio > (uint64_t)1 << 46) + ratio = (uint64_t)1 << 46; + + /* its root in 16.16 */ + ratio <<= 16; + root = 0; + for (bit = (uint64_t)1 << 62; bit != 0; bit >>= 2) { + if (ratio >= root + bit) { + ratio -= root + bit; + root = (root >> 1) + bit; + } else { + root >>= 1; + } + } + + a = root * mdct_norm * inv_noise_mult; + b = pow_table[value + 20]; + while (b >> 43) { /* keep the shift below in 64 bits */ + b >>= 1; + shift--; + } + b = (b << 20) / max_exponent; + + /* a * b >> shift, with both brought down to 31 bits first */ + while (a >> 31) { + a >>= 1; + shift--; + } + while (b >> 31) { + b >>= 1; + shift--; + } + a *= b; + if (shift > 0) + a >>= shift; + else if (a >> (31 + shift)) + return 0x7fffffff; + else + a <<= -shift; + + return a > 0x7fffffff ? 0x7fffffff : (fixed32)a; +} + /* return 0 if OK. return 1 if last block of frame. return -1 if unrecorrable error. */ static int wma_decode_block(WMADecodeContext *s) @@ -1064,7 +1127,7 @@ static int wma_decode_block(WMADecodeContext *s) fixed64 mult1; fixed32 noise, temp1, temp2, mult2; int i, j, n, n1, last_high_band, esize; - fixed32 exp_power[HIGH_BAND_MAX_SIZE]; + uint64_t exp_power[HIGH_BAND_MAX_SIZE]; //total_gain, coefs1, mdctnorm are lossless @@ -1098,14 +1161,15 @@ static int wma_decode_block(WMADecodeContext *s) for(i = 0;i < s->coefs_start; ++i) { *coefs++ = fixmul32( (fixmul32(s->noise_table[s->noise_index], - exponents[i<>esize])>>4),Fixed32From64(mult1)) >>2; + exponents[i<>esize])>>4),(fixed32)(mult1>>16)) >>2; s->noise_index = (s->noise_index + 1) & (NOISE_TAB_SIZE - 1); } n1 = s->exponent_high_sizes[bsize]; /* compute power of high bands */ - exponents = s->exponents[ch] +(s->high_band_start[bsize]<exponents[ch] + + (s->high_band_start[bsize]<>esize); last_high_band = 0; /* avoid warning */ for (j=0;jblock_len_bits][j]; if (s->high_band_coded[ch][j]) { - fixed32 e2, v; - e2 = 0; + uint64_t e2 = 0; for(i = 0;i < n; ++i) { - /*v is normalized later on so its fixed format is irrelevant*/ - v = exponents[i<>esize]>>4; - e2 += fixmul32(v, v)>>3; + /* Only ratios of the powers are used, so their + format does not matter; this one cannot + overflow. */ + uint32_t v = exponents[i<>esize]>>6; + e2 += (uint64_t)v * v; } - exp_power[j] = e2/n; /*n is an int...*/ + exp_power[j] = e2/n; last_high_band = j; } - exponents += n<>esize; } /* main freqs and high freqs */ - exponents = s->exponents[ch] + (s->coefs_start<exponents[ch] + (s->coefs_start<>esize); for(j=-1;j= 0 && s->high_band_coded[ch][j]) { /* use noise with specified power */ - fixed32 tmp = fixdiv32(exp_power[j],exp_power[last_high_band]); + fixed32 gain = noise_band_gain(exp_power[j], + exp_power[last_high_band], + s->high_band_values[ch][j], + s->max_exponent[ch], mdct_norm, + s->use_exp_vlc ? 50 : 25); - /*mult1 is 48.16, pow_table is 48.16*/ - mult1 = fixmul32(fixsqrt32(tmp), - pow_table[s->high_band_values[ch][j]+20]) >> 16; - - /*this step has a fairly high degree of error for some reason*/ - mult1 = fixdiv64(mult1,fixmul32(s->max_exponent[ch],s->noise_mult)); - mult1 = mult1*mdct_norm>>PRECISION; for(i = 0;i < n; ++i) { noise = s->noise_table[s->noise_index]; s->noise_index = (s->noise_index + 1) & (NOISE_TAB_SIZE - 1); *coefs++ = fixmul32((fixmul32(exponents[i<>esize],noise)>>4), - Fixed32From64(mult1)) >>2; + gain) >>2; } - exponents += n<>esize; } else { @@ -1176,7 +1238,7 @@ static int wma_decode_block(WMADecodeContext *s) temp2 = fixmul32(exponents[i<>esize], mult>>18); *coefs++ = fixmul32(temp1, temp2); } - exponents += n<>esize; } } From 9477e371a30050b493414c36280e0b53d34c9157 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 5 Oct 2026 20:43:43 -0400 Subject: [PATCH 362/408] wma: fix overflow in the LSP exponent curve Very low bitrate WMA files send their spectral envelope as LSP coefficients. The curve computed from them squares two products whose value can reach about a million, in 16.16 fixed point. Where the envelope should be lowest the squares overflowed, and those points came out near the curve's maximum instead: bands at the top of the spectrum up to 17 dB too loud, and everything else a few dB low, as levels are taken relative to the maximum. Sum the squares in 64 bits, and let pow_m1_4() take a value above the 16.16 range. Checked with perfsim (Sansa e200v1 build). The curve is within 0.2 dB of ffmpeg's formula at all 57600 points of 80 blocks traced from two files. Against ffmpeg's decode, as the mean difference in band levels over the whole file, with the noise coding fix applied before and after: StutteringFile (22 kHz mono, 20 kbps) 1.6 dB -> 0.3 dB shoujicomcn2117164 (same) 0.2 dB -> 0.1 dB terra8k (11 kHz mono, 8 kbps) 0.3 dB -> 0.0 dB Only the five files that use LSP exponents change; the other 23 are byte-identical. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libwma/wmadeci.c | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/rbcodec/codecs/libwma/wmadeci.c b/lib/rbcodec/codecs/libwma/wmadeci.c index a49b057d82..be304f0b53 100644 --- a/lib/rbcodec/codecs/libwma/wmadeci.c +++ b/lib/rbcodec/codecs/libwma/wmadeci.c @@ -587,7 +587,7 @@ int wma_decode_init(WMADecodeContext* s, asf_waveformatex_t *wfx) interpolation to reduce the mantissa table size at a small speed expense (linear interpolation approximately doubles the number of bits of precision). */ -static inline fixed32 pow_m1_4(WMADecodeContext *s, fixed32 x) +static inline fixed32 pow_m1_4(WMADecodeContext *s, fixed64 x) { union { float f; @@ -595,8 +595,18 @@ static inline fixed32 pow_m1_4(WMADecodeContext *s, fixed32 x) } u, t; unsigned int e, m; fixed32 a, b; + int shift = 0; - u.f = fixtof64(x); + /* x can be above what 16.16 holds; bring it into 32 bits to convert + it and put the shift back into the float's exponent */ + while (x >> 31) + { + x >>= 1; + shift++; + } + u.f = fixtof64((fixed32)x); + if (x != 0) + u.v += shift << 23; e = u.v >> 23; m = (u.v >> (23 - LSP_POW_BITS)) & ((1 << LSP_POW_BITS) - 1); /* build interpolation scale: 1 <= t < 2. */ @@ -654,6 +664,7 @@ static void wma_lsp_to_curve(WMADecodeContext *s, { int i, j; fixed32 p, q, w, v, val_max, temp2; + fixed64 v64; val_max = 0; for(i=0;i>9; /* p/q end up as 16.16 */ - v = pow_m1_4(s, v); + /* 2 in 5.27 format is 0x10000000. + * These squares are far above what 16.16 holds where the curve + * is low, so the sum is taken in 64 bits. It is in 16.16. */ + v64 = ((fixed64)p * fixmul32b(p, (0x10000000 - w)) + + (fixed64)q * fixmul32b(q, (0x10000000 + w))) >> 22; + v = pow_m1_4(s, v64); if (v > val_max) val_max = v; out[i] = v; From d084219b9a123e4981afecd6e6fa9ae22f0a8ad0 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 5 Oct 2026 19:53:02 -0400 Subject: [PATCH 363/408] vorbis: refuse streams with more than two channels Tremor is built here with CHANNELS 2: synthesis produces no PCM for a stream with more channels, so a 5.1 file "played" as nothing at all. It was still set up in full first, which took 416 to 880 KB of the codec heap for the 5.1 and 7.1 files tried, and wrote one entry per channel into arrays sized for two. Refuse such a stream when its identification header is read. The codec then returns an error at once, with under 8 KB of heap used. Checked with perfsim on the Sansa Clip+ model, not on a device: six 5.1 and 7.1 files are refused, and 13 stereo and mono files give the same PCM hashes as before. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libtremor/info.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/rbcodec/codecs/libtremor/info.c b/lib/rbcodec/codecs/libtremor/info.c index d922711e34..2e25877417 100644 --- a/lib/rbcodec/codecs/libtremor/info.c +++ b/lib/rbcodec/codecs/libtremor/info.c @@ -131,6 +131,11 @@ static int _vorbis_unpack_info(vorbis_info *vi,oggpack_buffer *opb){ if(vi->rate<1)goto err_out; if(vi->channels<1)goto err_out; + /* This build decodes at most CHANNELS channels (synthesis.c produces no + PCM for more, and several arrays are sized by it). Refuse the stream + here, before its setup is unpacked: a 5.1 stream's codebooks alone + can take several hundred KB of the codec heap for no output. */ + if(vi->channels>CHANNELS)goto err_out; if(ci->blocksizes[0]<64)goto err_out; if(ci->blocksizes[1]blocksizes[0])goto err_out; if(ci->blocksizes[1]>8192)goto err_out; From 34666474b8ed77ac88de81b23e6bcfc92209bc6d Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 5 Oct 2026 21:21:27 -0400 Subject: [PATCH 364/408] wma: fix dropouts in files with LSP exponents Where the LSP curve peaks, the value its -1/4 power is taken of is far below one step of 16.16 and was rounded to zero. pow_m1_4() of zero is enormous, the block's levels are taken relative to that maximum, and so the whole block came out about 46 dB down. In StutteringFile.wma (22 kHz mono, 20 kbps) this muted a block here and there, which is heard as a stutter. Pass the value to pow_m1_4() with all of its 38 fractional bits. Checked with perfsim (Sansa e200v1 build) against ffmpeg's decode of that file, in blocks of 46 ms: 11 of 841 were off by more than 1 dB, the worst by 10.6 dB; none are now, the worst by 0.4 dB. Four other files with LSP exponents are unchanged or closer, and files without them are byte-identical. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libwma/wmadeci.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/rbcodec/codecs/libwma/wmadeci.c b/lib/rbcodec/codecs/libwma/wmadeci.c index be304f0b53..fd852aa3b7 100644 --- a/lib/rbcodec/codecs/libwma/wmadeci.c +++ b/lib/rbcodec/codecs/libwma/wmadeci.c @@ -587,7 +587,8 @@ int wma_decode_init(WMADecodeContext* s, asf_waveformatex_t *wfx) interpolation to reduce the mantissa table size at a small speed expense (linear interpolation approximately doubles the number of bits of precision). */ -static inline fixed32 pow_m1_4(WMADecodeContext *s, fixed64 x) +/* x^(-1/4) in 16.16, for x given with frac_bits fractional bits */ +static inline fixed32 pow_m1_4(WMADecodeContext *s, fixed64 x, int frac_bits) { union { float f; @@ -597,16 +598,16 @@ static inline fixed32 pow_m1_4(WMADecodeContext *s, fixed64 x) fixed32 a, b; int shift = 0; - /* x can be above what 16.16 holds; bring it into 32 bits to convert - it and put the shift back into the float's exponent */ + /* bring x into 32 bits to convert it, and put that shift and the + fractional bits into the float's exponent */ while (x >> 31) { x >>= 1; shift++; } - u.f = fixtof64((fixed32)x); + u.f = (float)(fixed32)x; if (x != 0) - u.v += shift << 23; + u.v += (shift - frac_bits) << 23; e = u.v >> 23; m = (u.v >> (23 - LSP_POW_BITS)) & ((1 << LSP_POW_BITS) - 1); /* build interpolation scale: 1 <= t < 2. */ @@ -689,10 +690,11 @@ static void wma_lsp_to_curve(WMADecodeContext *s, /* 2 in 5.27 format is 0x10000000. * These squares are far above what 16.16 holds where the curve - * is low, so the sum is taken in 64 bits. It is in 16.16. */ - v64 = ((fixed64)p * fixmul32b(p, (0x10000000 - w)) + - (fixed64)q * fixmul32b(q, (0x10000000 + w))) >> 22; - v = pow_m1_4(s, v64); + * is low, and far below one of its steps where the curve peaks, + * so the sum is taken in 64 bits, with 38 fractional bits. */ + v64 = (fixed64)p * fixmul32b(p, (0x10000000 - w)) + + (fixed64)q * fixmul32b(q, (0x10000000 + w)); + v = pow_m1_4(s, v64, 38); if (v > val_max) val_max = v; out[i] = v; From 2890d4a40d59d1523ab8399c9d846fbc13645231 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Mon, 5 Oct 2026 19:53:02 -0400 Subject: [PATCH 365/408] vorbis: don't buffer the comment packet The comment packet is not used by the codec, but skipping it grew the stream's packet buffer to the packet's full size. Heap use rose with the size of the tags: a file with 300 KB of embedded album art needed 617 KB of heap, and one with 940 KB of art could not be opened even with a 992 KB codec buffer. Skip the packet without storing it: forget the part already buffered and let ogg_stream_pagein() drop the rest, as it does for a packet whose start was lost. Seeking to the start of a file ran the headers through the stream again, which buffered the whole comment packet by the normal path. Start the seek search at the first audio page instead, as libvorbis does, and handle a target inside that page. With both, heap use no longer depends on the tags. Large tags cost at most about 90 KB over an untagged file, from the two Ogg buffers growing to hold one full page each. Checked with perfsim on the Sansa Clip+ model, not on a device, with the heap limited to what a 512 KB codec buffer leaves: files with up to 1.5 MB of art or 240 KB of text decode to the same PCM hashes as the untagged audio, and 15 seeks per file, including to 0, 30 and 150 ms, give the same positions and PCM as the unmodified codec on ten files. Chained streams were not tested. Change-Id: Id27ebf98b436d813cfa3179951eee5bd7b2c0cd2 Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libtremor/vorbisfile.c | 68 +++++++++++++++++------ 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/lib/rbcodec/codecs/libtremor/vorbisfile.c b/lib/rbcodec/codecs/libtremor/vorbisfile.c index b9d8758a86..52a667c44c 100644 --- a/lib/rbcodec/codecs/libtremor/vorbisfile.c +++ b/lib/rbcodec/codecs/libtremor/vorbisfile.c @@ -130,25 +130,45 @@ static ogg_int64_t _get_next_page(OggVorbis_File *vf,ogg_page *og, } } -/* This is a nasty hack to work around the huge allocations we get from - huge comment packets, usually due to embedded album art */ +/* Skip the comment packet without storing it. It can be huge, usually due + to embedded album art, and none of it is used here. + + If only the start of the packet is buffered, that start is forgotten. + ogg_stream_pagein() then drops the rest of the packet by itself, as it + does for any packet whose beginning was lost, so the stream's body and + lacing buffers never grow with the size of the comment. */ static int ogg_stream_discard_packet(OggVorbis_File *vf,ogg_page *og, ogg_int64_t boundary){ - int ret; - while((ret = ogg_stream_packetout(&vf->os, NULL)) == 0) { - if(_get_next_page(vf, og, boundary)<0) - break; - ogg_stream_pagein(&vf->os,og,false); + ogg_stream_state *os=&vf->os; + int ret=ogg_stream_packetout(os,NULL); + int i; + + if(ret<0)return -1; + if(ret>0)return 1; /* the whole packet was buffered, and is now skipped */ + + /* forget the partial packet, as ogg_stream_pagein() does for a page + that is out of sequence */ + for(i=os->lacing_packet;ilacing_fill;i++) + os->body_fill-=os->lacing_vals[i]&0xff; + os->lacing_fill=os->lacing_packet; + + /* read pages until one holds the end of the packet: every lacing value + of a page that only continues it is 255 */ + for(;;){ + int segments; + + if(_get_next_page(vf,og,boundary)<0)return -1; + if(ogg_page_serialno(og)!=os->serialno)continue; + + ogg_stream_pagein(os,og,true); + + segments=og->header[26]; + for(i=0;iheader[27+i]<255)break; + if(ios.body_fill < og->body_len || vf->os.body_storage < vf->os.body_fill) - if(_os_body_expand(&vf->os, vf->os.body_fill - vf->os.body_storage + og->body_len)) - return -1; - memcpy(vf->os.body_data+vf->os.body_fill-og->body_len, og->body, og->body_len); + + os->packetno++; /* count the packet that was skipped */ return 1; } @@ -1287,11 +1307,16 @@ int ov_pcm_seek_page(OggVorbis_File *vf,ogg_int64_t pos){ /* new search algorithm by HB (Nicholas Vinen) */ { ogg_int64_t end=vf->offsets[link+1]; - ogg_int64_t begin=vf->offsets[link]; + /* Search from the first audio page, not from the start of the link + (as libvorbis does): a seek to the beginning must not run the + headers through the stream again, as that buffers the whole + comment packet, album art included. */ + ogg_int64_t begin=vf->dataoffsets[link]; ogg_int64_t begintime = vf->pcmlengths[link*2]; ogg_int64_t endtime = vf->pcmlengths[link*2+1]+begintime; ogg_int64_t target=pos-total+begintime; ogg_int64_t best=begin; + int found=0; /* a page that ends before the target */ ogg_page og; while(beginoffset; /* raw offset of next page */ begintime=granulepos; @@ -1391,8 +1417,14 @@ int ov_pcm_seek_page(OggVorbis_File *vf,ogg_int64_t pos){ ogg_stream_reset_serialno(&vf->os,vf->current_serialno); ogg_stream_pagein(&vf->os,&og,true); + /* The target is in the first audio page, which is where we are + now: decoding starts with its first packet, at the beginning of + the link. */ + if(!found) + vf->pcm_offset=total; + /* pull out all but last packet; the one with granulepos */ - while(1){ + while(found){ result=ogg_stream_packetpeek(&vf->os,&op); if(result==0){ /* !!! the packet finishing this page originated on a From dcee3ce62f0222719850ac033ec2ce2c34d63224 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Wed, 30 Sep 2026 09:29:02 -0400 Subject: [PATCH 366/408] jpeg: decode 16-bit quantization tables (SOF1) Both decoders rejected extended sequential JPEGs (SOF1), which are baseline files in all but name when they have 8-bit samples, and 16-bit quantization tables, which libjpeg writes for very low quality settings unless told to force baseline. Accept SOF1 with 8-bit samples, and read 16-bit table entries. The core loader keeps its tables in 16 bits and scales them for the IDCT, so it rejects entries over 8191; libjpeg's largest at quality 1 is 4950. Files using more than two Huffman tables are still rejected. Co-Authored-By: Claude Opus 5.5 Change-Id: I705a61402b4a0ab252995f8559b7ff2f68b05d72 --- apps/plugins/imageviewer/jpeg/jpeg_decoder.c | 21 ++++++++++++++------ apps/recorder/jpeg_load.c | 21 ++++++++++++-------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c index 98566affe8..39da52cc30 100644 --- a/apps/plugins/imageviewer/jpeg/jpeg_decoder.c +++ b/apps/plugins/imageviewer/jpeg/jpeg_decoder.c @@ -519,6 +519,7 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) break; case 0xC0: /* SOF Huff - Baseline DCT */ + case 0xC1: /* SOF Huff - Extended sequential DCT (8 bit samples) */ { ret |= SOF0; marker_size = *p_src++ << 8; /* Highbyte */ @@ -569,7 +570,6 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) } break; - case 0xC1: /* SOF Huff - Extended sequential DCT*/ case 0xC2: /* SOF Huff - Progressive DCT*/ case 0xC3: /* SOF Huff - Spatial (sequential) lossless*/ case 0xC5: /* SOF Huff - Differential sequential DCT*/ @@ -695,18 +695,27 @@ int process_markers(unsigned char* p_src, long size, struct jpeg* p_jpeg) ret |= DQT; marker_size = *p_src++ << 8; /* Highbyte */ marker_size |= *p_src++; /* Lowbyte */ - n = (marker_size-2)/(QUANT_TABLE_LENGTH+1); /* # of tables */ - for (i=0; i QUANT_TABLE_LENGTH) /* a table */ { - int id = *p_src++; /* ID */ - if (id >= 4) + int id = *p_src++; /* Pq: precision, Tq: ID */ + int pq = id >> 4; + id &= 0x0F; + if (id >= 4 || pq > 1 + || p_seg_end - p_src < QUANT_TABLE_LENGTH * (pq + 1)) { return (-8); /* Unsupported quantization table */ } /* Read Quantisation table: */ for (j=0; jquanttable[id][j] = *p_src++; + { + int q = *p_src++; + if (pq) /* 16 bit entries (SOF1) */ + q = q << 8 | *p_src++; + p_jpeg->quanttable[id][j] = q; + } } + p_src = p_seg_end; } break; diff --git a/apps/recorder/jpeg_load.c b/apps/recorder/jpeg_load.c index e81aba8ddf..334b407774 100644 --- a/apps/recorder/jpeg_load.c +++ b/apps/recorder/jpeg_load.c @@ -1019,6 +1019,7 @@ static int process_markers(struct jpeg* p_jpeg) break; /* discard */ case 0xC0: /* SOF Huff - Baseline DCT */ + case 0xC1: /* SOF Huff - Extended sequential DCT (8 bit samples) */ { JDEBUGF("SOF marker "); ret |= SOF0; @@ -1082,7 +1083,6 @@ static int process_markers(struct jpeg* p_jpeg) } break; - case 0xC1: /* SOF Huff - Extended sequential DCT*/ case 0xC2: /* SOF Huff - Progressive DCT*/ case 0xC3: /* SOF Huff - Spatial (sequential) lossless*/ case 0xC5: /* SOF Huff - Differential sequential DCT*/ @@ -1231,20 +1231,25 @@ static int process_markers(struct jpeg* p_jpeg) marker_size |= e_getc(p_jpeg, -1); /* Lowbyte */ marker_size -= 2; - n = (marker_size)/(QUANT_TABLE_LENGTH+1); /* # of tables */ - for (i=0; i QUANT_TABLE_LENGTH) /* another table */ { - int id = e_getc(p_jpeg, -1); /* ID */ - marker_size--; - if (id >= 4) + int id = e_getc(p_jpeg, -1); /* Pq: precision, Tq: ID */ + int pq = id >> 4; + id &= 0x0F; + marker_size -= 1 + QUANT_TABLE_LENGTH * (pq + 1); + if (id >= 4 || pq > 1 || marker_size < 0) { return (-8); /* Unsupported quantization table */ } /* Read Quantisation table: */ for (j=0; jquanttable[id][j] = e_getc(p_jpeg, -1); - marker_size--; + int q = e_getc(p_jpeg, -1); + if (pq) /* 16 bit entries (SOF1) */ + q = q << 8 | e_getc(p_jpeg, -1); + if (q > 8191) + return (-8); /* too big once scaled for the IDCT */ + p_jpeg->quanttable[id][j] = q; } } e_skip_bytes(p_jpeg, marker_size); From f1d7ae3bbf4ba295f3a69812c7f19f58f24b1503 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 6 Oct 2026 01:11:45 -0400 Subject: [PATCH 367/408] atrac3: give the decoder its frame size for RM files Since 2e314093c8 the ATRAC3 decoder takes its frame size from id3->bytesperframe, which the RM metadata parser does not set. With a frame size of zero, a file in plain stereo mode had its second channel decoded from the first channel's data: both outputs were the left channel. Set it from the RM sub packet size before the decoder is opened. Checked with perfsim (Sansa e200v1 build) on six ATRAC3 .rm files: the two channels were identical sample for sample before, and each now follows its own channel of ffmpeg's decode. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/atrac3_rm.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/rbcodec/codecs/atrac3_rm.c b/lib/rbcodec/codecs/atrac3_rm.c index 0ce961fd96..21be96587e 100644 --- a/lib/rbcodec/codecs/atrac3_rm.c +++ b/lib/rbcodec/codecs/atrac3_rm.c @@ -107,6 +107,10 @@ enum codec_status codec_run(void) scrambling_unit_size = h * (fs + packet_header_size); spn = h * fs / sps; + /* The decoder takes its frame size from the id3, which the RM + * metadata parser does not fill in. */ + ci->id3->bytesperframe = sps; + res = atrac3_decode_init(&q, ci->id3, rmctx.nb_channels, rmctx.extradata_size); if(res < 0) { DEBUGF("failed to initialize RM atrac decoder\n"); From 1bae7e0e0a50614fb3220bed117f5a4a7a3d63d1 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 6 Oct 2026 01:15:08 -0400 Subject: [PATCH 368/408] atrac3: reverse the whole of an odd band's spectrum The spectra of the odd QMF bands are stored reversed. 8722c6f2bb moved that reversal out of IMLT() and into the decoding of the coefficients, but did it within each scale factor band, one place off, and not at all for tonal components. The odd bands, which is 5.5 to 11 kHz and above 16.5 kHz at 44.1 kHz, have been decoded wrongly since: against ffmpeg's decode that range was uncorrelated. Reverse the whole band in IMLT() again, as ffmpeg does. The old commit measured the saving it is giving up as 0.11 MHz on PP5024. Checked with perfsim (Sansa e200v1 build) against ffmpeg's decode of an OMA file and six RM files: the coherence of the 5.7 to 10.7 kHz range goes from under 0.01 to 0.99, the same as other bands. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libatrac/atrac3.c | 48 ++++++++++++---------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/lib/rbcodec/codecs/libatrac/atrac3.c b/lib/rbcodec/codecs/libatrac/atrac3.c index f0098258b3..877e2fbfc6 100644 --- a/lib/rbcodec/codecs/libatrac/atrac3.c +++ b/lib/rbcodec/codecs/libatrac/atrac3.c @@ -297,8 +297,17 @@ static void iqmf (int32_t *inlo, int32_t *inhi, unsigned int nIn, int32_t *pOut, * @param odd_band 1 if the band is an odd band */ -static void IMLT(int32_t *pInput, int32_t *pOutput) +static void IMLT(int32_t *pInput, int32_t *pOutput, int odd_band) { + if (odd_band) { + /* Reverse the odd bands before the IMDCT; this is an effect of the + * QMF transform. The whole band is reversed, tonal components + * included, so it cannot be done as the coefficients are decoded. */ + int i; + for (i = 0; i < 128; i++) + FFSWAP(int32_t, pInput[i], pInput[255-i]); + } + /* Apply the imdct. */ ff_imdct_calc(9, pOutput, pInput); @@ -438,31 +447,16 @@ static void inverseQuantizeSpectrum(int *mantissas, int32_t *pOut, int *pIn = mantissas; /* Inverse quantize the coefficients. */ - if((first/256) &1) { - /* Odd band - Reverse coefficients */ - do { - pOut[last--] = fixmul16(*pIn++, SF); - pOut[last--] = fixmul16(*pIn++, SF); - pOut[last--] = fixmul16(*pIn++, SF); - pOut[last--] = fixmul16(*pIn++, SF); - pOut[last--] = fixmul16(*pIn++, SF); - pOut[last--] = fixmul16(*pIn++, SF); - pOut[last--] = fixmul16(*pIn++, SF); - pOut[last--] = fixmul16(*pIn++, SF); - } while (last>first); - } else { - /* Even band - Do not reverse coefficients */ - do { - pOut[first++] = fixmul16(*pIn++, SF); - pOut[first++] = fixmul16(*pIn++, SF); - pOut[first++] = fixmul16(*pIn++, SF); - pOut[first++] = fixmul16(*pIn++, SF); - pOut[first++] = fixmul16(*pIn++, SF); - pOut[first++] = fixmul16(*pIn++, SF); - pOut[first++] = fixmul16(*pIn++, SF); - pOut[first++] = fixmul16(*pIn++, SF); - } while (firstspectrum[band*256]), pSnd->IMDCT_buf); + IMLT(&(pSnd->spectrum[band*256]), pSnd->IMDCT_buf, band & 1); } else { memset(pSnd->IMDCT_buf, 0, 512 * sizeof(int32_t)); } From b7170e03c4671d2296d1526508afd6e06c7ece89 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 6 Oct 2026 01:15:08 -0400 Subject: [PATCH 369/408] atrac3: apply the whole mdct window Only the first and last 128 points of the 512 point window were applied, and the 256 between were taken to be one. They are not: the window rises to 1.207 there. Every block was therefore up to 1.6 dB low over half its length, which left the output 0.7 dB low on average and an error about 23 dB below the signal at all frequencies. Add the missing half of the table and apply it. Checked with perfsim (Sansa e200v1 build) against ffmpeg's decode, with the two previous fixes applied before and after: level SNR five plain stereo RM files -0.7 dB 23 dB -> 0.0 dB 54-57 dB joint stereo RM file -0.7 dB 23 dB -> 0.0 dB 37-45 dB joint stereo OMA file -0.7 dB 23 dB -> 0.0 dB 45 dB Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libatrac/atrac3.c | 7 ++++- .../codecs/libatrac/atrac3data_fixed.h | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/rbcodec/codecs/libatrac/atrac3.c b/lib/rbcodec/codecs/libatrac/atrac3.c index 877e2fbfc6..ea3b02fc86 100644 --- a/lib/rbcodec/codecs/libatrac/atrac3.c +++ b/lib/rbcodec/codecs/libatrac/atrac3.c @@ -252,11 +252,16 @@ atrac3_imdct_windowing(int32_t *buffer, const int32_t *win) { int32_t i; - /* win[0..127] = win[511..384], win[128..383] = 1 */ + /* win[0..127] = win[511..384] */ for(i = 0; i<128; i++) { buffer[ i] = fixmul31(win[i], buffer[ i]); buffer[511-i] = fixmul31(win[i], buffer[511-i]); } + /* win[128..255] = win[383..256] = 1 + window_lookup_mid[] */ + for(i = 0; i<128; i++) { + buffer[128+i] += fixmul31(window_lookup_mid[i], buffer[128+i]); + buffer[383-i] += fixmul31(window_lookup_mid[i], buffer[383-i]); + } } diff --git a/lib/rbcodec/codecs/libatrac/atrac3data_fixed.h b/lib/rbcodec/codecs/libatrac/atrac3data_fixed.h index 3cc4e0837c..54ec832880 100644 --- a/lib/rbcodec/codecs/libatrac/atrac3data_fixed.h +++ b/lib/rbcodec/codecs/libatrac/atrac3data_fixed.h @@ -76,6 +76,35 @@ static const int32_t window_lookup[128] ICONST_ATTR MEM_ALIGN_ATTR = { -0x82660c01, -0x80ca4a01, }; +/* The window above is the first 128 points of the 512 point mdct window. + * Points 128..255 are these values plus one, scaled by 2^31; they rise to + * 1.207 and fall back to 1. The second half of the window mirrors the + * first. */ +static const int32_t window_lookup_mid[128] ICONST_ATTR MEM_ALIGN_ATTR = { + 0x00c7d1c8, 0x024fd962, 0x03cd6738, 0x05401bc8, 0x06a79e95, 0x08039e75, + 0x0953d1d1, 0x0a97f6df, 0x0bcfd3c6, 0x0cfb36c3, 0x0e19f636, 0x0f2bf0a8, + 0x10310cc9, 0x11293955, 0x12146d01, 0x12f2a64e, 0x13c3eb59, 0x148849a0, + 0x153fd5c0, 0x15eaab2c, 0x1688ebd9, 0x171abfeb, 0x17a05559, 0x1819df8c, + 0x188796ff, 0x18e9b8db, 0x1940868e, 0x198c456d, 0x19cd3e43, 0x1a03bcf6, + 0x1a301021, 0x1a5288af, 0x1a6b7984, 0x1a7b371a, 0x1a821730, 0x1a807072, + 0x1a769a2c, 0x1a64ebfc, 0x1a4bbd8d, 0x1a2b6653, 0x1a043d52, 0x19d698de, + 0x19a2ce6d, 0x19693261, 0x192a17e5, 0x18e5d0be, 0x189cad2d, 0x184efbcf, + 0x17fd0985, 0x17a72158, 0x174d8c6b, 0x16f091e8, 0x169076f2, 0x162d7ea1, + 0x15c7e9f4, 0x155ff7cf, 0x14f5e4fc, 0x1489ec23, 0x141c45d4, 0x13ad2886, + 0x133cc89d, 0x12cb5870, 0x12590851, 0x11e60697, 0x11727fa4, 0x10fe9df7, + 0x108a8a2e, 0x10166b1a, 0x0fa265c6, 0x0f2e9d86, 0x0ebb3404, 0x0e48494e, + 0x0dd5fbe2, 0x0d6468bc, 0x0cf3ab63, 0x0c83ddf9, 0x0c151945, 0x0ba774c2, + 0x0b3b06af, 0x0acfe414, 0x0a6620d7, 0x09fdcfc5, 0x0997029e, 0x0931ca20, + 0x08ce3615, 0x086c555d, 0x080c35f8, 0x07ade513, 0x07516f11, 0x06f6df92, + 0x069e4182, 0x06479f20, 0x05f30203, 0x05a0732a, 0x054ffafe, 0x0501a15d, + 0x04b56d9f, 0x046b66a1, 0x042392c9, 0x03ddf80e, 0x039a9bfc, 0x035983bd, + 0x031ab41f, 0x02de3196, 0x02a40042, 0x026c23f9, 0x0236a043, 0x02037867, + 0x01d2af6a, 0x01a44813, 0x017844f1, 0x014ea85c, 0x0127747d, 0x0102ab49, + 0x00e04e8c, 0x00c05fe5, 0x00a2e0cf, 0x0087d29c, 0x006f367d, 0x00590d81, + 0x00455897, 0x00341890, 0x00254e20, 0x0018f9df, 0x000f1c4a, 0x0007b5c4, + 0x0002c697, 0x00004ef5, +}; + /* Gain tables scaled by 2^16 */ static const int32_t gain_tab2[31] ICONST_ATTR = { 0x0003ab03, 0x00035d14, 0x0003159d, 0x0002d414, 0x000297fb, 0x000260e0, From 8667af5cd7ca2d57bc2ece016878ab0afce1f193 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 6 Oct 2026 10:46:37 -0400 Subject: [PATCH 370/408] atrac3: fix gain compensation at adjacent gain points applyVariableGain() applied the constant gain before a gain point in a do-while loop, so at least eight samples of it, even when there are none: when the point is at the start of the block or follows straight after the previous one. Every later gain point in the block was then eight samples late. This came in with the loop unrolling of 51a8be1a0f. Use a while loop, as ffmpeg does. Checked with perfsim (Sansa e200v1 build) against ffmpeg's decode. Over the whole of atrac3_lp2_132.oma the SNR goes from 45 to 51 dB and the worst frame from 14 to 21 dB; the right channel of a joint stereo RM file goes from 37 to 54 dB. What is left is a noise floor near -72 dBFS in every frame, from the 2 fractional bits the decoder works with. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libatrac/atrac3.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/rbcodec/codecs/libatrac/atrac3.c b/lib/rbcodec/codecs/libatrac/atrac3.c index ea3b02fc86..fc7d03e621 100644 --- a/lib/rbcodec/codecs/libatrac/atrac3.c +++ b/lib/rbcodec/codecs/libatrac/atrac3.c @@ -717,8 +717,10 @@ static int applyVariableGain (int32_t *pIn, int32_t *pPrev, int32_t *pOut, { int32_t i = start; - /* Apply fix gains until end index is reached */ - do { + /* Apply fix gains until end index is reached. There is nothing to do + * here when the gain point is where the previous one ended, or at the + * start of the block. */ + while (i < end) { pOut[i] = fixmul16((fixmul16(pIn[i], gain1) + pPrev[i]), gain2); i++; pOut[i] = fixmul16((fixmul16(pIn[i], gain1) + pPrev[i]), gain2); i++; pOut[i] = fixmul16((fixmul16(pIn[i], gain1) + pPrev[i]), gain2); i++; @@ -727,7 +729,7 @@ static int applyVariableGain (int32_t *pIn, int32_t *pPrev, int32_t *pOut, pOut[i] = fixmul16((fixmul16(pIn[i], gain1) + pPrev[i]), gain2); i++; pOut[i] = fixmul16((fixmul16(pIn[i], gain1) + pPrev[i]), gain2); i++; pOut[i] = fixmul16((fixmul16(pIn[i], gain1) + pPrev[i]), gain2); i++; - } while (i < end); + } /* Interpolation is done over next eight samples */ pOut[i] = fixmul16((fixmul16(pIn[i], gain1) + pPrev[i]), gain2); i++; From b8861e3f091e234881e972ed6d89cbddace9d880 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 6 Oct 2026 11:49:45 -0400 Subject: [PATCH 371/408] atrac3: remove the rounding bias of the ARMv5E QMF filter The ARMv5E dewindowing routine takes 16 of each product's bits, rounded down, so each sum of 24 products came out about 12 units low. Through the filter bank that put a dc offset of about 12 LSB (of 16 bits) on the output and, from the high band, a tone at half the sample rate; together they were four fifths of the decoder's error on these targets. Start the sums 12 up. Also correct a comment in both ARM files. Checked with perfsim (Sansa Clip+ build) against ffmpeg's decode of seven files: the dc offset goes from 11 to 12 LSB down to under 1 LSB, and the error from about -66 dBFS to -69 to -72 dBFS, the same as the ARMv4 routine gives. Decoding is 0.7% slower (26.73 to 26.91 MHz for atrac3_lp2_132.oma). The ARMv4 output is unchanged. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libatrac/atrac3_arm.S | 2 +- lib/rbcodec/codecs/libatrac/atrac3_armv5e.S | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/rbcodec/codecs/libatrac/atrac3_arm.S b/lib/rbcodec/codecs/libatrac/atrac3_arm.S index 3a81039d2b..aa60329ded 100644 --- a/lib/rbcodec/codecs/libatrac/atrac3_arm.S +++ b/lib/rbcodec/codecs/libatrac/atrac3_arm.S @@ -161,7 +161,7 @@ atrac3_iqmf_dewindowing: orr r8, r12, r8, lsl #1 /* s2 = low>>31 || hi<<1 */ stmia r0!, {r8, r9} /* store result out[0]=s2, out[1]=s1 */ - sub r1, r1, #184 /* roll back 64 entries = 184 bytes */ + sub r1, r1, #184 /* roll back 46 entries = 184 bytes */ sub r2, r2, #192 /* roll back 48 entries = 192 bytes = win[0] */ subs r3, r3, #1 /* outer loop -= 1 */ diff --git a/lib/rbcodec/codecs/libatrac/atrac3_armv5e.S b/lib/rbcodec/codecs/libatrac/atrac3_armv5e.S index d5ba92c55d..8bfaca2410 100644 --- a/lib/rbcodec/codecs/libatrac/atrac3_armv5e.S +++ b/lib/rbcodec/codecs/libatrac/atrac3_armv5e.S @@ -66,8 +66,13 @@ atrac3_iqmf_dewindowing_armv5e: /* 0.. 7 */ ldmia r2!, {r4, r5, r8, r9} /* load win[0..7] */ ldmia r1!, {r6, r7, r10, r11} /* load in[0..3] to avoid stall on arm11 */ - smulwb lr, r6, r4 /* s1 = in[0] * win[0] */ - smulwt r12, r7, r4 /* s2 = in[1] * win[1] */ + /* Each of the 24 products of a sum is rounded down by half a unit on + * average. Start the sums 12 up to make up for it; without that the + * output has a dc offset and, from the high band, a tone at fs/2. */ + mov lr, #12 + mov r12, #12 + smlawb lr, r6, r4, lr /* s1 += in[0] * win[0] >> 16 */ + smlawt r12, r7, r4, r12 /* s2 += in[1] * win[1] >> 16 */ smlawb lr, r10, r5, lr /* s1 += in[i ] * win[i ] >> 16 */ smlawt r12, r11,r5, r12 /* s2 += in[i+1] * win[i+1] >> 16 */ @@ -152,7 +157,7 @@ atrac3_iqmf_dewindowing_armv5e: mov r12, r12, lsl #1 stmia r0!, {r12, lr} /* store result out[0]=s2, out[1]=s1 */ - sub r1, r1, #184 /* roll back 64 entries = 184 bytes */ + sub r1, r1, #184 /* roll back 46 entries = 184 bytes */ sub r2, r2, #96 /* roll back 48 entries * 2 bytes = 96 bytes = win[0] */ subs r3, r3, #1 /* outer loop -= 1 */ From 3966b420a3bb5d0d42b30665039213ddb7deb315 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 6 Oct 2026 11:51:49 -0400 Subject: [PATCH 372/408] atrac3: use the QMF window's symmetry in the ARMv4 filter The dewindowing loop is 72% of ATRAC3 decoding on ARM7TDMI. The window is symmetric, so read only its first half: each pair of coefficients serves two inputs from the front of the 48 and, with the two swapped, two from the back. That halves the coefficient loads and lets them be done four at a time. The sums are kept in 64 bits, so the new order of the additions does not change the result: the PCM output of seven files is byte-identical. Estimated with perfsim for the Sansa e200v1 (not measured on the device), atrac3_lp2_132.oma: 53.74 -> 48.81 MHz. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libatrac/atrac3_arm.S | 95 +++++++++++------------- 1 file changed, 44 insertions(+), 51 deletions(-) diff --git a/lib/rbcodec/codecs/libatrac/atrac3_arm.S b/lib/rbcodec/codecs/libatrac/atrac3_arm.S index aa60329ded..29011a2598 100644 --- a/lib/rbcodec/codecs/libatrac/atrac3_arm.S +++ b/lib/rbcodec/codecs/libatrac/atrac3_arm.S @@ -92,48 +92,35 @@ atrac3_iqmf_matrixing: * in += 2; * out += 2; * } + * + * The window is symmetric, win[i] = win[47-i], so only its first half is + * read: win[2k] and win[2k+1] serve in[2k] and in[2k+1] from the front of + * the 48 inputs and, swapped, in[46-2k] and in[47-2k] from the back. That + * is 24 coefficient loads for an output pair where there were 48, and + * they can be loaded four at a time. The sums are kept in 64 bits, so the + * order of the additions does not change the result. + * * Note: r12 is a scratch register and can be used without restorage. ****************************************************************************/ -/* To be called as first block to call smull for initial filling of the result - * registers lr/r9 and r12/r8. */ -#define DEWIN_8_SAMPLES_MUL_ASM \ - ldmia r2!, {r4, r5}; /* load win[0..1] */ \ - ldmia r1!, {r6, r7}; /* load in [0..1] */ \ - smull lr , r9, r4, r6; /* s1 = win[0] * in[0] */ \ - smull r12, r8, r5, r7; /* s2 = win[1] * in[1] */ \ - ldmia r2!, {r4, r5}; /* load win[i..i+1] */ \ - ldmia r1!, {r6, r7}; /* load in [i..i+1] */ \ - smlal lr , r9, r4, r6; /* s1 = win[i ] * in[i ] */ \ - smlal r12, r8, r5, r7; /* s2 = win[i+1] * in[i+1] */ \ - ldmia r2!, {r4, r5}; /* load win[i..i+1] */ \ - ldmia r1!, {r6, r7}; /* load in [i..i+1] */ \ - smlal lr , r9, r4, r6; /* s1 = win[i ] * in[i ] */ \ - smlal r12, r8, r5, r7; /* s2 = win[i+1] * in[i+1] */ \ - ldmia r2!, {r4, r5}; /* load win[i..i+1] */ \ - ldmia r1!, {r6, r7}; /* load in [i..i+1] */ \ - smlal lr , r9, r4, r6; /* s1 = win[i ] * in[i ] */ \ - smlal r12, r8, r5, r7; /* s2 = win[i+1] * in[i+1] */ - -/* Called after first block. Will always multiply-add to the result registers - * lr/r9 and r12/r8. */ -#define DEWIN_8_SAMPLES_MLA_ASM \ - ldmia r2!, {r4, r5}; /* load win[i..i+1] */ \ - ldmia r1!, {r6, r7}; /* load in [i..i+1] */ \ - smlal lr , r9, r4, r6; /* s1 = win[i ] * in[i ] */ \ - smlal r12, r8, r5, r7; /* s2 = win[i+1] * in[i+1] */ \ - ldmia r2!, {r4, r5}; /* load win[i..i+1] */ \ - ldmia r1!, {r6, r7}; /* load in [i..i+1] */ \ - smlal lr , r9, r4, r6; /* s1 = win[i ] * in[i ] */ \ - smlal r12, r8, r5, r7; /* s2 = win[i+1] * in[i+1] */ \ - ldmia r2!, {r4, r5}; /* load win[i..i+1] */ \ - ldmia r1!, {r6, r7}; /* load in [i..i+1] */ \ - smlal lr , r9, r4, r6; /* s1 = win[i ] * in[i ] */ \ - smlal r12, r8, r5, r7; /* s2 = win[i+1] * in[i+1] */ \ - ldmia r2!, {r4, r5}; /* load win[i..i+1] */ \ - ldmia r1!, {r6, r7}; /* load in [i..i+1] */ \ - smlal lr , r9, r4, r6; /* s1 = win[i ] * in[i ] */ \ - smlal r12, r8, r5, r7; /* s2 = win[i+1] * in[i+1] */ +/* Eight taps: coefficients win[2k..2k+3], two input pairs from the front + * (r1) and two from the back (r3). s1 is lr/r9 and s2 is r12/r8. The + * samples are the last operand of the multiplies, as they are the smaller + * one and that is what the multiplier's early termination goes by. */ +#define DEWIN_8_SAMPLES_ASM(first1, first2) \ + ldmia r2!, {r4-r7}; /* load win[2k..2k+3] */ \ + ldmia r1!, {r10, r11}; /* load in[2k], in[2k+1] */ \ + first1 lr , r9, r4, r10; /* s1 += win[2k ] * in[2k ] */ \ + first2 r12, r8, r5, r11; /* s2 += win[2k+1] * in[2k+1] */ \ + ldmdb r3!, {r10, r11}; /* load in[46-2k], in[47-2k] */ \ + smlal lr , r9, r5, r10; /* s1 += win[2k+1] * in[46-2k] */ \ + smlal r12, r8, r4, r11; /* s2 += win[2k ] * in[47-2k] */ \ + ldmia r1!, {r10, r11}; /* load in[2k+2], in[2k+3] */ \ + smlal lr , r9, r6, r10; /* s1 += win[2k+2] * in[2k+2] */ \ + smlal r12, r8, r7, r11; /* s2 += win[2k+3] * in[2k+3] */ \ + ldmdb r3!, {r10, r11}; /* load in[44-2k], in[45-2k] */ \ + smlal lr , r9, r7, r10; /* s1 += win[2k+3] * in[44-2k] */ \ + smlal r12, r8, r6, r11; /* s2 += win[2k+2] * in[45-2k] */ .align 2 .global atrac3_iqmf_dewindowing @@ -144,16 +131,19 @@ atrac3_iqmf_dewindowing: /* r1 = input samples */ /* r2 = window coefficients */ /* r3 = counter */ - stmfd sp!, {r4-r9, lr} /* save non-scratch registers */ + stmfd sp!, {r4-r11, lr} /* save non-scratch registers */ + add r3, r0, r3, lsl #3 /* end of dest: two words a count */ + stmfd sp!, {r3} + add r3, r1, #192 /* r3 = end of the 48 inputs */ .iqmf_dewindow_outer_loop: /* outer loop 0...counter-1 */ - DEWIN_8_SAMPLES_MUL_ASM /* 0.. 7, use "MUL" macro here! */ - DEWIN_8_SAMPLES_MLA_ASM /* 8..15 */ - DEWIN_8_SAMPLES_MLA_ASM /* 16..23 */ - DEWIN_8_SAMPLES_MLA_ASM /* 24..31 */ - DEWIN_8_SAMPLES_MLA_ASM /* 32..39 */ - DEWIN_8_SAMPLES_MLA_ASM /* 40..47 */ + DEWIN_8_SAMPLES_ASM(smull, smull) /* in[ 0.. 3], in[44..47] */ + DEWIN_8_SAMPLES_ASM(smlal, smlal) /* in[ 4.. 7], in[40..43] */ + DEWIN_8_SAMPLES_ASM(smlal, smlal) /* in[ 8..11], in[36..39] */ + DEWIN_8_SAMPLES_ASM(smlal, smlal) /* in[12..15], in[32..35] */ + DEWIN_8_SAMPLES_ASM(smlal, smlal) /* in[16..19], in[28..31] */ + DEWIN_8_SAMPLES_ASM(smlal, smlal) /* in[20..23], in[24..27] */ mov lr , lr , lsr #31 orr r9, lr , r9, lsl #1 /* s1 = low>>31 || hi<<1 */ @@ -161,13 +151,16 @@ atrac3_iqmf_dewindowing: orr r8, r12, r8, lsl #1 /* s2 = low>>31 || hi<<1 */ stmia r0!, {r8, r9} /* store result out[0]=s2, out[1]=s1 */ - sub r1, r1, #184 /* roll back 46 entries = 184 bytes */ - sub r2, r2, #192 /* roll back 48 entries = 192 bytes = win[0] */ + ldr r4, [sp] /* end of dest */ + sub r1, r1, #88 /* back 24 entries, on 2: in += 2 */ + add r3, r3, #104 /* on 24 entries, and 2 more */ + sub r2, r2, #96 /* roll back 24 entries = win[0] */ - subs r3, r3, #1 /* outer loop -= 1 */ - bgt .iqmf_dewindow_outer_loop + cmp r0, r4 + bne .iqmf_dewindow_outer_loop - ldmpc regs=r4-r9 /* restore registers */ + add sp, sp, #4 + ldmpc regs=r4-r11 /* restore registers */ .atrac3_iqmf_dewindowing_end: .size atrac3_iqmf_dewindowing,.atrac3_iqmf_dewindowing_end-atrac3_iqmf_dewindowing From 02d5520c5f004d9d32c1263b47eb0103657c0040 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 6 Oct 2026 21:33:31 -0400 Subject: [PATCH 373/408] atrac3: output samples with 28 fractional bits The decoder gave the DSP samples with two fractional bits (sample depth 17), far fewer than any other codec. The DSP's filters are only as precise as the samples they are given: with an equalizer band at a low frequency the output was noise, or held at full scale. Scale the last stage of the synthesis filter up by 11 bits and set the sample depth to 28. On ARMv4 the bits are taken from the 64-bit sums of the dewindowing, which had them. Elsewhere the input of that stage is scaled, in its matrixing step; that is not done on ARMv4 because its multiplier takes longer for the larger operands (51.97 against 48.85 MHz below). The Coldfire path uses the C matrixing and has not been run. Seven files decoded under perfsim for the Sansa e200v1 and Clip+: shifted down again, the e200v1 output is within one step of the old output, and the Clip+ output no further from the e200v1's than before. Through a ten band equalizer with bands at 32 and 64 Hz (the filter fix of the DSP included), the noise of atrac3_lp2_132.oma falls from -64 dB to -101 dB relative to the signal. Estimated with perfsim (not measured on a device), atrac3_lp2_132.oma: e200v1 48.81 -> 48.85 MHz, Clip+ 26.91 -> 26.96 MHz. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/atrac3_oma.c | 2 +- lib/rbcodec/codecs/atrac3_rm.c | 2 +- lib/rbcodec/codecs/libatrac/atrac3.c | 56 ++++++++++++++-- lib/rbcodec/codecs/libatrac/atrac3.h | 7 ++ lib/rbcodec/codecs/libatrac/atrac3_arm.S | 84 ++++++++++++++++++++---- 5 files changed, 133 insertions(+), 18 deletions(-) diff --git a/lib/rbcodec/codecs/atrac3_oma.c b/lib/rbcodec/codecs/atrac3_oma.c index ede11840d1..e9d9dda3c6 100644 --- a/lib/rbcodec/codecs/atrac3_oma.c +++ b/lib/rbcodec/codecs/atrac3_oma.c @@ -63,7 +63,7 @@ enum codec_status codec_run(void) ci->memset(&q,0,sizeof(ATRAC3Context)); ci->configure(DSP_SET_FREQUENCY, ci->id3->frequency); - ci->configure(DSP_SET_SAMPLE_DEPTH, 17); /* Remark: atrac3 uses s15.0 by default, s15.2 was hacked. */ + ci->configure(DSP_SET_SAMPLE_DEPTH, ATRAC3_OUTPUT_DEPTH); const uint8_t channels = 2; ci->configure(DSP_SET_STEREO_MODE, channels == 1 ? STEREO_MONO : STEREO_NONINTERLEAVED); diff --git a/lib/rbcodec/codecs/atrac3_rm.c b/lib/rbcodec/codecs/atrac3_rm.c index 21be96587e..1d42beb11d 100644 --- a/lib/rbcodec/codecs/atrac3_rm.c +++ b/lib/rbcodec/codecs/atrac3_rm.c @@ -92,7 +92,7 @@ enum codec_status codec_run(void) init_rm(&rmctx); ci->configure(DSP_SET_FREQUENCY, ci->id3->frequency); - ci->configure(DSP_SET_SAMPLE_DEPTH, 17); /* Remark: atrac3 uses s15.0 by default, s15.2 was hacked. */ + ci->configure(DSP_SET_SAMPLE_DEPTH, ATRAC3_OUTPUT_DEPTH); ci->configure(DSP_SET_STEREO_MODE, rmctx.nb_channels == 1 ? STEREO_MONO : STEREO_NONINTERLEAVED); diff --git a/lib/rbcodec/codecs/libatrac/atrac3.c b/lib/rbcodec/codecs/libatrac/atrac3.c index fc7d03e621..f814b2b626 100644 --- a/lib/rbcodec/codecs/libatrac/atrac3.c +++ b/lib/rbcodec/codecs/libatrac/atrac3.c @@ -86,6 +86,13 @@ static int vlcs_initialized = 0; int32_t *inlo, int32_t *inhi, unsigned int nIn); + /* The same, with the results scaled up by ATRAC3_OUT_SHIFT bits + * (ARMv5 and later) */ + extern void + atrac3_iqmf_matrixing_out(int32_t *p3, + int32_t *inlo, + int32_t *inhi, + unsigned int nIn); #else static inline void atrac3_iqmf_matrixing(int32_t *p3, @@ -101,6 +108,22 @@ static int vlcs_initialized = 0; p3[2*i+3] = inlo[i+1] - inhi[i+1]; } } + + /* The same, with the results scaled up by ATRAC3_OUT_SHIFT bits */ + static inline void + atrac3_iqmf_matrixing_out(int32_t *p3, + int32_t *inlo, + int32_t *inhi, + unsigned int nIn) + { + uint32_t i; + for(i=0; ipUnits[i].delayBuf1, q->tempBuf); - iqmf (p4, p3, 256, p3, q->pUnits[i].delayBuf2, q->tempBuf); - iqmf (p1, p3, 512, p1, q->pUnits[i].delayBuf3, q->tempBuf); + iqmf (p1, p2, 256, p1, q->pUnits[i].delayBuf1, q->tempBuf, false); + iqmf (p4, p3, 256, p3, q->pUnits[i].delayBuf2, q->tempBuf, false); + iqmf (p1, p3, 512, p1, q->pUnits[i].delayBuf3, q->tempBuf, true); p1 +=1024; } diff --git a/lib/rbcodec/codecs/libatrac/atrac3.h b/lib/rbcodec/codecs/libatrac/atrac3.h index 68db7af9d1..661e4810aa 100644 --- a/lib/rbcodec/codecs/libatrac/atrac3.h +++ b/lib/rbcodec/codecs/libatrac/atrac3.h @@ -35,6 +35,13 @@ #define ICONST_ATTR_LARGE_IRAM #endif +/* The decoder works on samples with two fractional bits, s15.2. The last + * stage of the synthesis filter is scaled up by this many more bits, so the + * output has 28 bits below the sign like that of most codecs and the + * precision of that stage's multiplications is not thrown away. */ +#define ATRAC3_OUT_SHIFT 11 +#define ATRAC3_OUTPUT_DEPTH (17 + ATRAC3_OUT_SHIFT) + /* These structures are needed to store the parsed gain control data. */ typedef struct { int num_gain_data; diff --git a/lib/rbcodec/codecs/libatrac/atrac3_arm.S b/lib/rbcodec/codecs/libatrac/atrac3_arm.S index 29011a2598..70181e1352 100644 --- a/lib/rbcodec/codecs/libatrac/atrac3_arm.S +++ b/lib/rbcodec/codecs/libatrac/atrac3_arm.S @@ -71,6 +71,58 @@ atrac3_iqmf_matrixing: .atrac3_iqmf_matrixing_end: .size atrac3_iqmf_matrixing,.atrac3_iqmf_matrixing_end-atrac3_iqmf_matrixing + +/* ATRAC3_OUT_SHIFT of atrac3.h */ +#define OUT_SHIFT 11 + +/**************************************************************************** + * void atrac3_iqmf_matrixing_out(int32_t *dest, + * int32_t *inlo, + * int32_t *inhi, + * unsigned int count); + * + * The matrixing step of the last iqmf stage: as atrac3_iqmf_matrixing, with + * the results scaled up by OUT_SHIFT bits, which then is the scale of the + * decoder's output. Not used on ARMv4, where the multiplications of the + * dewindowing would take longer for it: see atrac3_iqmf_dewindowing_out. + ****************************************************************************/ +#if ARM_ARCH >= 5 + .align 2 + .global atrac3_iqmf_matrixing_out + .type atrac3_iqmf_matrixing_out, %function + +atrac3_iqmf_matrixing_out: + /* r0 = dest */ + /* r1 = inlo */ + /* r2 = inhi */ + /* r3 = counter */ + stmfd sp!, {r4-r9, lr} /* save non-scratch registers */ + +.iqmf_matrixing_out_loop: + ldmia r1!, { r4, r6, r8, r12} /* load inlo[0...3] */ + ldmia r2!, { r5, r7, r9, lr } /* load inhi[0...3] */ + add r4, r4, r5 /* r4 = inlo[0] + inhi[0] */ + mov r4, r4, asl #OUT_SHIFT + sub r5, r4, r5, asl #OUT_SHIFT+1 /* r5 = inlo[0] - inhi[0] */ + add r6, r6, r7 /* r6 = inlo[1] + inhi[1] */ + mov r6, r6, asl #OUT_SHIFT + sub r7, r6, r7, asl #OUT_SHIFT+1 /* r7 = inlo[1] - inhi[1] */ + add r8, r8, r9 /* r8 = inlo[2] + inhi[2] */ + mov r8, r8, asl #OUT_SHIFT + sub r9, r8, r9, asl #OUT_SHIFT+1 /* r9 = inlo[2] - inhi[2] */ + add r12, r12, lr /* r12 = inlo[3] + inhi[3] */ + mov r12, r12, asl #OUT_SHIFT + sub lr , r12, lr, asl #OUT_SHIFT+1 /* lr = inlo[3] - inhi[3] */ + stmia r0!, {r4-r9, r12, lr} /* store results to dest */ + subs r3, r3, #4 /* counter -= 4 */ + bgt .iqmf_matrixing_out_loop + + ldmpc regs=r4-r9 /* restore registers */ + +.atrac3_iqmf_matrixing_out_end: + .size atrac3_iqmf_matrixing_out,.atrac3_iqmf_matrixing_out_end-atrac3_iqmf_matrixing_out +#endif /* ARM_ARCH >= 5 */ + /**************************************************************************** * atrac3_iqmf_dewindowing(int32_t *out, @@ -122,11 +174,16 @@ atrac3_iqmf_matrixing: smlal lr , r9, r7, r10; /* s1 += win[2k+3] * in[44-2k] */ \ smlal r12, r8, r6, r11; /* s2 += win[2k+2] * in[45-2k] */ +/* atrac3_iqmf_dewindowing is the function above. atrac3_iqmf_dewindowing_out + * is the same for the last iqmf stage, with the result scaled up by OUT_SHIFT + * bits, which then is the scale of the decoder's output: the bits are taken + * from the 64-bit sums, that have them. */ +.macro DEWINDOWING name, shift .align 2 - .global atrac3_iqmf_dewindowing - .type atrac3_iqmf_dewindowing, %function - -atrac3_iqmf_dewindowing: + .global \name + .type \name, %function + +\name: /* r0 = dest */ /* r1 = input samples */ /* r2 = window coefficients */ @@ -136,7 +193,7 @@ atrac3_iqmf_dewindowing: stmfd sp!, {r3} add r3, r1, #192 /* r3 = end of the 48 inputs */ -.iqmf_dewindow_outer_loop: /* outer loop 0...counter-1 */ +1: /* outer loop 0...counter-1 */ DEWIN_8_SAMPLES_ASM(smull, smull) /* in[ 0.. 3], in[44..47] */ DEWIN_8_SAMPLES_ASM(smlal, smlal) /* in[ 4.. 7], in[40..43] */ @@ -145,10 +202,10 @@ atrac3_iqmf_dewindowing: DEWIN_8_SAMPLES_ASM(smlal, smlal) /* in[16..19], in[28..31] */ DEWIN_8_SAMPLES_ASM(smlal, smlal) /* in[20..23], in[24..27] */ - mov lr , lr , lsr #31 - orr r9, lr , r9, lsl #1 /* s1 = low>>31 || hi<<1 */ - mov r12, r12, lsr #31 - orr r8, r12, r8, lsl #1 /* s2 = low>>31 || hi<<1 */ + mov lr , lr , lsr #31-\shift + orr r9, lr , r9, lsl #1+\shift /* s1 = low>>31 || hi<<1, << shift */ + mov r12, r12, lsr #31-\shift + orr r8, r12, r8, lsl #1+\shift /* s2 = low>>31 || hi<<1, << shift */ stmia r0!, {r8, r9} /* store result out[0]=s2, out[1]=s1 */ ldr r4, [sp] /* end of dest */ @@ -157,10 +214,13 @@ atrac3_iqmf_dewindowing: sub r2, r2, #96 /* roll back 24 entries = win[0] */ cmp r0, r4 - bne .iqmf_dewindow_outer_loop + bne 1b add sp, sp, #4 ldmpc regs=r4-r11 /* restore registers */ -.atrac3_iqmf_dewindowing_end: - .size atrac3_iqmf_dewindowing,.atrac3_iqmf_dewindowing_end-atrac3_iqmf_dewindowing + .size \name, .-\name +.endm + + DEWINDOWING atrac3_iqmf_dewindowing, 0 + DEWINDOWING atrac3_iqmf_dewindowing_out, OUT_SHIFT From fc1d445d691c638fbcdf71e2dc56d7de1c4cde7c Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 6 Oct 2026 22:18:33 -0400 Subject: [PATCH 374/408] atrac3: keep the second window table out of small IRAM b7170e03c4 added window_lookup_mid, 512 bytes, to IRAM on every target. On the targets with 48KB of IRAM for a codec that left the ATRAC3 codecs 48 bytes too large to link (iPod Color and the other PP502x players with 96KB of IRAM). Put the table in IRAM only on the targets with more, like the decoder's other large tables. Nothing changes there. Built for iPod Color, where the two codecs now have 476 bytes of IRAM to spare, and for the iriver H120. Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/codecs/libatrac/atrac3data_fixed.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/rbcodec/codecs/libatrac/atrac3data_fixed.h b/lib/rbcodec/codecs/libatrac/atrac3data_fixed.h index 54ec832880..2bc1316547 100644 --- a/lib/rbcodec/codecs/libatrac/atrac3data_fixed.h +++ b/lib/rbcodec/codecs/libatrac/atrac3data_fixed.h @@ -79,8 +79,9 @@ static const int32_t window_lookup[128] ICONST_ATTR MEM_ALIGN_ATTR = { /* The window above is the first 128 points of the 512 point mdct window. * Points 128..255 are these values plus one, scaled by 2^31; they rise to * 1.207 and fall back to 1. The second half of the window mirrors the - * first. */ -static const int32_t window_lookup_mid[128] ICONST_ATTR MEM_ALIGN_ATTR = { + * first. In IRAM only where there is room: with it the codec does not fit + * the 48KB of the smaller targets. */ +static const int32_t window_lookup_mid[128] ICONST_ATTR_LARGE_IRAM MEM_ALIGN_ATTR = { 0x00c7d1c8, 0x024fd962, 0x03cd6738, 0x05401bc8, 0x06a79e95, 0x08039e75, 0x0953d1d1, 0x0a97f6df, 0x0bcfd3c6, 0x0cfb36c3, 0x0e19f636, 0x0f2bf0a8, 0x10310cc9, 0x11293955, 0x12146d01, 0x12f2a64e, 0x13c3eb59, 0x148849a0, From e27fb9a9f08e57f3d20601dc1548a0c1d4320b6c Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 6 Oct 2026 21:50:09 -0400 Subject: [PATCH 375/408] test_codec: add checksum runs with the DSP "Checksum with DSP" and "Checksum folder with DSP" give the CRC32 of the DSP's 16-bit output for a file or a folder, as "Checksum" does for the codec's output. That lets the DSP of a device, with its settings, be checked against a reference for several files in one run; "Write WAV with DSP" writes one file, /test.wav. Co-Authored-By: Claude Opus 5.5 --- apps/plugins/test_codec.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/plugins/test_codec.c b/apps/plugins/test_codec.c index b38ae221e0..0a54cacc41 100644 --- a/apps/plugins/test_codec.c +++ b/apps/plugins/test_codec.c @@ -918,6 +918,8 @@ enum plugin_status plugin_start(const void* parameter) WRITE_WAV_WITH_DSP, CHECKSUM, CHECKSUM_DIR, + CHECKSUM_WITH_DSP, + CHECKSUM_DIR_WITH_DSP, QUIT, #ifdef HAVE_ADJUSTABLE_CPU_FREQ BOOST, @@ -934,6 +936,8 @@ enum plugin_status plugin_start(const void* parameter) "Write WAV with DSP", "Checksum", "Checksum folder", + "Checksum with DSP", + "Checksum folder with DSP", "Quit", #ifdef HAVE_ADJUSTABLE_CPU_FREQ "Boosting", @@ -967,6 +971,13 @@ menu: scandir = 0; + /* Map checksum runs with DSP to CHECKSUM and CHECKSUM_DIR: the + * checksum is then that of the DSP's output. */ + bool dsp_checksum = (result == CHECKSUM_WITH_DSP || + result == CHECKSUM_DIR_WITH_DSP); + if (dsp_checksum) + result -= 2; + /* Map test runs with checksum calcualtion to standard runs * SPEED_TEST and SPEED_TEST_DIR and set the 'checksum' flag. */ if ((checksum = (result == CHECKSUM || @@ -978,6 +989,8 @@ menu: if ((use_dsp = (result >= SPEED_TEST_WITH_DSP && result <= WRITE_WAV_WITH_DSP))) result -= 3; + else + use_dsp = dsp_checksum; if (result == SPEED_TEST) { wavinfo.fd = -1; From 4c08906670feb39079f324e584b57476c3b223ab Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 6 Oct 2026 22:10:24 -0400 Subject: [PATCH 376/408] test_codec: set the DSP's output samplerate The DSP gets its output samplerate from the codec thread when a track is played. test_codec did not set it, so its runs with the DSP used the rate of the last track played, or the default if there was none: after "Playback frequency" was changed, they resampled to the old rate until something had been played. Set it from the mixer for each file, as the codec thread does. Co-Authored-By: Claude Opus 5.5 --- apps/plugins/test_codec.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/plugins/test_codec.c b/apps/plugins/test_codec.c index 0a54cacc41..9ba111f4bd 100644 --- a/apps/plugins/test_codec.c +++ b/apps/plugins/test_codec.c @@ -726,6 +726,11 @@ static enum plugin_status test_track(const char* filename) if (use_dsp) { rb->dsp_configure(ci.dsp, DSP_RESET, 0); + /* The DSP is told its output samplerate when a track is played. + * Tell it here as well, or it is that of the last track and not + * what playback would use now. */ + rb->dsp_configure(ci.dsp, DSP_SET_OUT_FREQUENCY, + rb->mixer_get_frequency()); rb->dsp_configure(ci.dsp, DSP_FLUSH, 0); } From 31fac4c400cc9b296569bff8dab223af9e341033 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 6 Oct 2026 20:16:01 -0400 Subject: [PATCH 377/408] configure: drop the 'bootloader only' from the ipodnano3g This allows the themesite to validate this target Change-Id: I77407098ec0c92c845e5e14f4d6bfcd3fb230ebb --- tools/configure | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/configure b/tools/configure index 47c620dc7e..75215dd46e 100755 --- a/tools/configure +++ b/tools/configure @@ -2634,7 +2634,6 @@ fi 80|ipodnano3g) target_id=117 modelname="ipodnano3g" - blonly="yes" target="IPOD_NANO3G" memory=32 # always arm926ejscc From 9d3d2c6e677f608ce3e33950e4d6f3b5f51bce9e Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Tue, 6 Oct 2026 21:17:13 -0400 Subject: [PATCH 378/408] checkwps: validate themes against builtin cabbiev2 assets Themes for a given device often reference the assets of the default cabbiev2 theme. Record a list of what is bundled, and check anything not included with the theme against that list. If something is still missing, then that's an error. This applies to fonts, wps files, backdrops, icons, and so forth. Change-Id: I5ff7feaae892c8b9a81c2cb3035879672f3e6c29 --- tools/checkwps/checkwps.c | 45 +++++++++++++++++++++++++++++------- tools/checkwps/checkwps.make | 13 ++++++++++- tools/root.make | 2 +- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/tools/checkwps/checkwps.c b/tools/checkwps/checkwps.c index a62105fc12..a80c081d56 100644 --- a/tools/checkwps/checkwps.c +++ b/tools/checkwps/checkwps.c @@ -227,6 +227,7 @@ bool radio_hardware_present(void) #endif #include "fontbundle.h" +#include "themeassets.h" static int loaded_fonts = 0; static struct font _font; @@ -277,29 +278,57 @@ static int checkwps_loader(const char *path, const char *search, int mode) int missing = 1; if (final) *final = 0; - /* Check if font is included in the bundle */ - for (int i = 0 ; bundledfonts[i] != NULL ; i++) + /* Check if font is a default theme asset */ + for (int i = 0 ; themeassets[i] != NULL; i++) { - if (first && !strcmp(first+1, bundledfonts[i])) + if (first && !strcmp(first+1, themeassets[i])) { missing = 0; break; } } + /* Check if font is included in the bundle */ if (missing) + { + for (int i = 0 ; bundledfonts[i] != NULL ; i++) + { + if (first && !strcmp(first+1, bundledfonts[i])) + { + missing = 2; + break; + } + } + } + + if (missing == 1) { printf("Font missing >%s<\n", first+1); return -4; - } else { + } else if (missing == 2) { printf("INFO: Theme requires rockbox font bundle\n"); } } - - if (mode == 2) + else if (mode == 2) /* Not found and it's an asset */ { - printf("ERROR: File missing: %s\n", path); - return -5; + char *first = strrchr(buf, '/'); + char *final = strrchr(buf, '.'); + int missing = 1; + if (final) *final = 0; + + for (int i = 0 ; themeassets[i] != NULL; i++) + { + if (first && !strcmp(first+1, themeassets[i])) + { + missing = 0; + break; + } + } + if (missing) + { + printf("ERROR: File missing: %s\n", path); + return -5; + } } } diff --git a/tools/checkwps/checkwps.make b/tools/checkwps/checkwps.make index c103cdcc47..86384dee7c 100644 --- a/tools/checkwps/checkwps.make +++ b/tools/checkwps/checkwps.make @@ -42,11 +42,22 @@ $(BUILDDIR)/$(BINARY): $$(CHECKWPS_OBJ) $$(CORE_LIBS) $(BUILDDIR)/fontbundle.h: $(ROOTDIR)/fonts/*bdf @echo FONTBUNDLE - $(SILENT)echo "static unsigned char* bundledfonts[] = {" > $@ + $(SILENT)echo "static const unsigned char* bundledfonts[] = {" > $@ $(SILENT)ls $(ROOTDIR)/fonts/*bdf | perl -pne 's|.*/(\d+-.*)\.bdf| "$$1",|;' >> $@ $(SILENT)echo " NULL, " >> $@ $(SILENT)echo "};" >> $@ +$(BUILDDIR)/themeassets.h: $(ROOTDIR)/firmware/export/config/$(MODELNAME).h $(ROOTDIR)/wps/WPSLIST + @echo THEMEASSETS + $(SILENT)rm -Rf $(BUILDDIR)/.rockbox + $(SILENT)mkdir $(BUILDDIR)/.rockbox + $(SILENT)echo "static const unsigned char* themeassets[] = {" > $@ + $(SILENT)$(ROOTDIR)/wps/wpsbuild.pl $(ROOTDIR)/wps/WPSLIST $(TARGET) > /dev/null 2>&1 + $(SILENT)find $(BUILDDIR)/.rockbox -type f | perl -pne 's|.*/(\S+)\.\S+| "$$1",|;' >> $@ + $(SILENT)echo " NULL, " >> $@ + $(SILENT)echo "};" >> $@ + $(SILENT)rm -Rf $(BUILDDIR)/.rockbox + #### Everything below is hacked in from apps.make and lang.make $(BUILDDIR)/apps/features: $(ROOTDIR)/apps/features.txt diff --git a/tools/root.make b/tools/root.make index 878ecd1c36..d2ff41eab4 100644 --- a/tools/root.make +++ b/tools/root.make @@ -236,7 +236,7 @@ clean:: $(LINKRAM) $(LINKROM) rockbox.elf rockbox.map rockbox.bin \ make.dep rombox.elf rombox.map rombox.bin romstart.txt \ $(BINARY) $(FLASHFILE) uisimulator bootloader flash $(BOOTLINK) \ - rockbox.apk lang_enum.h rbversion.h fontbundle.h 3ds rockbox.3dsx \ + rockbox.apk lang_enum.h rbversion.h fontbundle.h themeassets.h 3ds rockbox.3dsx \ rockbox.bnr rockbox.cia rockbox.icn rockbox.smdh #### linking the binaries: #### From 9ce3fca4cb3d5c04b2a434c2629207e7bac6cf99 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 7 Oct 2026 08:01:21 +0200 Subject: [PATCH 379/408] rk27xx: no whole-cache invalidate at run time commit_discard_idcache() invalidated both cache ways at run time, at every codec and plugin load and every USB connect. With the cache on, the CPU crashed in the poll loop when that loop started a cache line of its own; with the cache off around it, since 0e1952a4d1, code fetched again afterwards could come back wrong. On a Samsung YP-CP3 a build with a few changes elsewhere took an undefined instruction exception in the USB interrupt handler at every boot into USB mode, always at the same instruction, wherever the linker put it. The same build with only the invalidate at USB connect replaced by a nop booted and worked. The original firmware invalidates the ways only once, at power-on with the cache off, as crt0.S does, and after that only single lines. None is needed at run time: the cache is unified and write-through, so what the CPU writes, code included, is in memory and in any cached copy, and every DMA into memory - SD reads, USB receives, recording - discards the lines of its own buffer first. NAND is read by the CPU. So commit_discard_idcache(), and commit_discard_dcache() with it, now do nothing. Tested on a YP-CP3, with the build that crashed: it boots into USB mode, copies files with matching checksums, plays several formats, runs plugins (fft with playback, bubbles), and records. Co-Authored-By: Claude Opus 5.5 Change-Id: I14301a0c05736e28e53a3365c838f4a4a73052b3 --- firmware/target/arm/rk27xx/system-rk27xx.c | 35 ++++++---------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/firmware/target/arm/rk27xx/system-rk27xx.c b/firmware/target/arm/rk27xx/system-rk27xx.c index 0d4a521cf8..8ca7d3d25f 100644 --- a/firmware/target/arm/rk27xx/system-rk27xx.c +++ b/firmware/target/arm/rk27xx/system-rk27xx.c @@ -184,34 +184,17 @@ void udelay(unsigned usecs) ); } -static void cache_invalidate_way(int way) -{ - /* Issue invalidata way command to the cache controler */ - CACHEOP = ((way<<31)|0x2); - - /* wait for invalidate process to complete */ - while (CACHEOP & 0x03); -} - +/* No whole-cache invalidate at run time, as in the original firmware: it + * invalidates the ways once, at power-on with the cache off (crt0.S), and + * after that only single lines. Invalidating the ways while running from + * cached SDRAM crashed depending on where code happened to lie - with the + * cache on in the poll loop, and with it off later, as code was fetched + * again. None is needed: the cache is unified and write-through, so what + * the CPU writes, code included, is in memory and in any cached copy, and + * every DMA into memory discards the lines of its own buffer first + * (commit_discard_dcache_range()). */ void commit_discard_idcache(void) { - int old_irq = disable_irq_save(); - unsigned long devid = DEVID; - - /* Invalidate with the cache off, as crt0 does. This code runs from - * cached SDRAM: invalidating the ways while fetching through them fails - * when the poll loop starts on a cache line of its own, depending on - * where the linker happened to put it. The cache is write-through, so - * nothing is lost by turning it off. */ - DEVID = devid & ~(1UL << 31); - - cache_invalidate_way(0); - - cache_invalidate_way(1); - - DEVID = devid; - - restore_irq(old_irq); } void commit_discard_dcache (void) __attribute__((alias("commit_discard_idcache"))); From 136e6b99539f11762839bf333fe3a88c04c46798 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 7 Oct 2026 08:20:30 +0200 Subject: [PATCH 380/408] recording: stop monitoring the input before USB mode Plugging in USB on the recording screen closes recording, but the input stays selected: the screen switches it back to playback only when it ends, which is after USB mode, at the unplug. On a Samsung YP-CP3, whose codec passes the microphone or FM radio through to the headphones while recording, the input could be heard all through USB mode. Switch to playback before entering it. Tested on a YP-CP3. Co-Authored-By: Claude Opus 5.5 Change-Id: I194d5e120cc39685aa43cea4741e826a86ed2c65 --- apps/recorder/recording.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/recorder/recording.c b/apps/recorder/recording.c index 4f7ccc1be9..1a8058b089 100644 --- a/apps/recorder/recording.c +++ b/apps/recorder/recording.c @@ -1540,6 +1540,9 @@ bool recording_screen(bool no_source) { FOR_NB_SCREENS(i) screens[i].set_viewport(NULL); + /* not monitoring the input in USB mode: this screen + * only switches back to playback when it ends */ + rec_set_source(AUDIO_SRC_PLAYBACK, SRCF_PLAYBACK); default_event_handler(SYS_USB_CONNECTED); done = true; rec_status |= RCSTAT_BEEN_IN_USB_MODE; From e61aa0cd37d39531ca54175d17a9dd2cfce11637 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 7 Oct 2026 08:02:00 +0200 Subject: [PATCH 381/408] usb_storage: allocate the buffer at the storage handover The host configures the device before Rockbox has handed the storage over, and mass storage allocated its buffer right then. On the recording screen the recording buffer still holds all free memory at that point and cannot shrink, so plugging in USB there panicked with "usb_storage_init_connection(): OOM". Recording closes, and frees its buffer, only on SYS_USB_CONNECTED, which the handover broadcasts. Playback does not hit this: its buffer gives memory up on request. The buffer is only needed to run commands, and commands already wait for the handover. So it is allocated, and the endpoint primed for the first command, in the notify event that both handover paths in usb.c send, or at once when the storage is already handed over. Until then the host's first command waits at the endpoint, NAKed. GET_MAX_LUN, which comes before the handover, is answered from the core's control buffer instead of the transfer buffer. Targets with static USB buffers are unchanged. Tested on a Samsung YP-CP3: USB plugged in on the recording screen, at boot and from the main menu, mounts, and files copied both ways keep their checksums. Co-Authored-By: Claude Opus 5.5 Change-Id: Icadc24fd52a07d03125234e492dab0ede9e339c9 --- firmware/usbstack/usb_storage.c | 43 ++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/firmware/usbstack/usb_storage.c b/firmware/usbstack/usb_storage.c index 2598a3d46d..a262770e4a 100644 --- a/firmware/usbstack/usb_storage.c +++ b/firmware/usbstack/usb_storage.c @@ -449,9 +449,13 @@ static int usb_storage_get_config_descriptor(unsigned char *dest,int max_packet_ static int usb_handle = 0; #endif -static int usb_storage_init_connection(void) +/* Sets up the buffers and primes the rx endpoint for the first command. + * The buffers are allocated only once Rockbox has handed the storage over: + * the host configures the device before every thread has acknowledged the + * connect, while for example the recording buffer still holds all free + * memory. Until then the host's first command waits at the endpoint. */ +static void start_receiving_commands(void) { - logf("ums: set config"); /* prime rx endpoint. We only need room for commands */ state = WAITING_FOR_COMMAND; @@ -470,6 +474,9 @@ static int usb_storage_init_connection(void) #else unsigned char * buffer; + if (usb_handle > 0) + return; /* already receiving */ + // Add 31 to handle worst-case misalignment usb_handle = core_alloc_ex(ALLOCATE_BUFFER_SIZE + MAX_CBW_SIZE + 31, &buflib_ops_locked); @@ -489,6 +496,25 @@ static int usb_storage_init_connection(void) #endif #endif usb_drv_recv_nonblocking(EP_OUT, cbw_buffer, MAX_CBW_SIZE); +} + +static bool receiving_commands(void) +{ +#ifdef USB_STATIC_ALLOC + return true; +#else + return usb_handle > 0; +#endif +} + +static int usb_storage_init_connection(void) +{ + logf("ums: set config"); + state = WAITING_FOR_COMMAND; +#ifndef USB_STATIC_ALLOC + if(usb_exclusive_storage()) +#endif + start_receiving_commands(); int i; for(i=0;ibRequest) { case USB_BULK_GET_MAX_LUN: { - *tb.max_lun = storage_num_drives() - 1; + /* comes before the handover, without the transfer buffer */ + reqdata[0] = storage_num_drives() - 1; #if defined(HAVE_MULTIDRIVE) - if(skip_first) (*tb.max_lun) --; + if(skip_first) reqdata[0]--; #endif logf("ums: getmaxlun"); - usb_core_control_response(USB_CONTROL_ACK, tb.max_lun, 1); + usb_core_control_response(USB_CONTROL_ACK, reqdata, 1); handled = true; break; } @@ -1450,6 +1476,11 @@ static void handle_scsi(struct command_block_wrapper* cbw) static void usb_storage_notify_event(intptr_t data) { (void)data; + if(!receiving_commands()) { + if(usb_exclusive_storage()) + start_receiving_commands(); + return; + } if(state == WAITING_FOR_STORAGE && usb_exclusive_storage()) handle_scsi((struct command_block_wrapper*)cbw_buffer); } From c3a62cee966c0a7d3a1f77c6a9577b795d4f841a Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 7 Oct 2026 16:03:44 +0200 Subject: [PATCH 382/408] rk27xx: pass the NAND bootloader's handoff words to the OF Before it jumps to an image, the rk27xx NAND bootloader writes three words at the address in RKW header field 0x14: a magic, its version and which copy of the image it loaded. The original firmware never initialises them: it reports the version over USB and counts its own reboots in the third word, and once the count reaches 5 it reboots into a mode the NAND bootloader will not boot. When our bootloader started the OF, nothing wrote them, so the OF ran with whatever was left in DRAM. Field 0x14 of our own images held a constant taken from some other image. Point it at the last 12 bytes of DRAM, which nothing uses before our bootloader runs. load_rkw() in the bootloader now reads the words there before loading an image, and writes them at the address in the loaded image's header when that lies between the image and the bootloader. Started over USB, it passes version 0. Co-Authored-By: Claude Opus 5.5 Change-Id: Ife1d63e361a780c28a9f0626d110552b98bb6b7f --- firmware/export/rkw-loader.h | 4 +- firmware/target/arm/rk27xx/rkw-loader.c | 86 +++++++++++++++++++++++++ tools/rkw.c | 10 ++- 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/firmware/export/rkw-loader.h b/firmware/export/rkw-loader.h index 2101bd03b7..076aca2eba 100644 --- a/firmware/export/rkw-loader.h +++ b/firmware/export/rkw-loader.h @@ -30,7 +30,9 @@ struct rkw_header_t { uint32_t image_base; /* Base address of the firmware image */ uint32_t load_address; /* Load address */ uint32_t load_limit; /* End of the firmware image */ - uint32_t bss_start; /* This is the start of .bss section of the firmware I suppose */ + uint32_t handoff; /* Where the loader leaves its three handoff + * words for the image (see rkw-loader.c) + */ uint32_t reserved0; /* reserved - I've seen only zeros in this field so far */ uint32_t reserved1; /* reserved - I've seen only zeros in this field so far */ uint32_t entry_point; /* Entry point address */ diff --git a/firmware/target/arm/rk27xx/rkw-loader.c b/firmware/target/arm/rk27xx/rkw-loader.c index 9b50e1b187..c7f694e8a0 100644 --- a/firmware/target/arm/rk27xx/rkw-loader.c +++ b/firmware/target/arm/rk27xx/rkw-loader.c @@ -19,6 +19,7 @@ ****************************************************************************/ #include +#include #include "config.h" #include "loader_strerror.h" #include "rkw-loader.h" @@ -26,6 +27,82 @@ #include "file.h" #include "panic.h" +#ifdef BOOTLOADER +/* Before the NAND bootloader jumps to an image it writes three words at + * the address in header field 0x14: a magic, its own version and which + * copy of the image it loaded. The OF never initialises them; it reports + * the version over USB and counts its reboots in the third word. When we + * start the OF ourselves we pass on what the NAND bootloader gave us, so + * the OF sees the same as when it is started directly. + */ +#define RKW_HANDOFF_MAGIC 0x03df479a +/* written over the magic once the words have been read */ +#define RKW_HANDOFF_CONSUMED 0x123456ad + +/* The last 12 bytes of DRAM. tools/rkw.c puts this address in every + * Rockbox RKW, so the NAND bootloader leaves our words here. + */ +#define RKW_HANDOFF_ADDR (0x60000000 + MEMORYSIZE * 0x100000 - 12) +#if MEMORYSIZE != 16 +#error "tools/rkw.c assumes 16 MB of DRAM, update RKW_HANDOFF_ADDR there" +#endif + +struct rkw_handoff_t { + uint32_t magic; + uint32_t version; + uint32_t source; +}; + +/* The NAND bootloader's words, read once: loading an image may overwrite + * them, and a reboot keeps DRAM, so they are marked consumed. + */ +static struct rkw_handoff_t *rkw_own_handoff(void) +{ + static struct rkw_handoff_t own; + static bool read_done = false; + volatile struct rkw_handoff_t *h = + (volatile struct rkw_handoff_t *)RKW_HANDOFF_ADDR; + + if (!read_done) + { + if (h->magic == RKW_HANDOFF_MAGIC) + { + own.version = h->version; + own.source = h->source; + } + else + { + /* not started by the NAND bootloader, e.g. over USB */ + own.version = 0; + own.source = 0; + } + own.magic = RKW_HANDOFF_MAGIC; + h->magic = RKW_HANDOFF_CONSUMED; + read_done = true; + } + + return &own; +} + +/* Hand the words on to an image loaded at its own address, when its + * header points past the image and below us. + */ +static void rkw_pass_handoff(const struct rkw_header_t *hdr, + const unsigned char *buf, int len, + int buffer_size) +{ + uintptr_t addr = hdr->handoff; + uintptr_t lo = (uintptr_t)buf + len; + uintptr_t hi = (uintptr_t)buf + buffer_size; + + if ((uintptr_t)buf != hdr->load_address || (addr & 3) || + addr < lo || addr > hi - sizeof(struct rkw_handoff_t)) + return; + + *(struct rkw_handoff_t *)addr = *rkw_own_handoff(); +} +#endif /* BOOTLOADER */ + /* loosely based on load_firmware() * on success we return size of loaded image * on error we return negative value which can be deciphered by means @@ -116,6 +193,11 @@ int load_rkw(unsigned char* buf, const char* firmware, int buffer_size) goto end; } +#ifdef BOOTLOADER + /* before the image can overwrite them */ + rkw_own_handoff(); +#endif + /* skip header */ lseek(fd, sizeof(rkw_info), SEEK_SET); @@ -141,6 +223,10 @@ int load_rkw(unsigned char* buf, const char* firmware, int buffer_size) } } +#ifdef BOOTLOADER + rkw_pass_handoff(&rkw_info, buf, len, buffer_size); +#endif + ret = len; end: close(fd); diff --git a/tools/rkw.c b/tools/rkw.c index db86b4f748..6d5f4c61d3 100644 --- a/tools/rkw.c +++ b/tools/rkw.c @@ -26,6 +26,14 @@ #define RKLD_MAGIC 0x4c44524b #define RKW_HEADER_SIZE 0x2c +/* The NAND bootloader writes three handoff words at the address in + * header field 0x14 before it jumps to the image. Point it at the last + * 12 bytes of DRAM, which nothing touches before our bootloader reads + * them. Every rk27xx target has 16 MB; keep in step with + * RKW_HANDOFF_ADDR in firmware/target/arm/rk27xx/rkw-loader.c. + */ +#define RKW_HANDOFF_ADDR (0x60000000 + 16 * 0x100000 - 12) + /* slightly modified version from crc32.c in rockbox */ static uint32_t rkw_crc32(const void *src, uint32_t len) { @@ -114,7 +122,7 @@ int rkw_encode(char *iname, char *oname, unsigned long modelnum) int2le(0x60000000, outbuf+0x08); /* base address */ int2le(0x60000000, outbuf+0x0c); /* load address */ int2le(0x60000000+length, outbuf+0x10); /* end address */ - int2le(0x6035a5e4, outbuf+0x14); /* points to some unknown struct */ + int2le(RKW_HANDOFF_ADDR, outbuf+0x14); /* loader handoff words */ int2le(modelnum, outbuf+0x18); /* reserved (we abuse the format * to store modelnum here */ From 406a28cc6ab61dacca4cba747c0bb1eef53a3c24 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 7 Oct 2026 16:03:44 +0200 Subject: [PATCH 383/408] rk27xx: boot the OF when it asked for a reboot Before the original firmware resets itself, e.g. at the end of a firmware update, it stores a boot mode in an undocumented GPIO1 register that keeps its value over the reset. The NAND bootloader reads it to decide whether to boot at all, then starts our bootloader, which chose Rockbox or the OF from the buttons alone, so an OF that restarted itself came back up as Rockbox. Rockbox never writes the register and it reads 0 after power-on, so boot the OF whenever it is set. Co-Authored-By: Claude Opus 5.5 Change-Id: I22a98cafad48f66302a8401c9df847a073675a58 --- bootloader/rk27xx.c | 8 ++++++++ firmware/export/rk27xx.h | 2 ++ 2 files changed, 10 insertions(+) diff --git a/bootloader/rk27xx.c b/bootloader/rk27xx.c index 4f04bebfc8..db4fd675ac 100644 --- a/bootloader/rk27xx.c +++ b/bootloader/rk27xx.c @@ -139,6 +139,14 @@ void main(void) if (DEV_INFO & (1<<20)) boot = of; + /* The OF asks for a mode before it resets itself, e.g. at the end of + * a firmware update, and the cell survives the reset. Rockbox never + * sets it, so after a cold start it reads 0: anything else is a + * request from the OF, which expects to come back up. + */ + if (GPIO1_BOOTMODE & 0xff) + boot = of; + lcd_clear_display(); ret = storage_init(); diff --git a/firmware/export/rk27xx.h b/firmware/export/rk27xx.h index 4579a8914e..5ec71f3f8c 100644 --- a/firmware/export/rk27xx.h +++ b/firmware/export/rk27xx.h @@ -429,6 +429,8 @@ #define GPIO_PECON (*(volatile unsigned long *)(APB0_GPIO1 + 0x04)) #define GPIO_PFDR (*(volatile unsigned long *)(APB0_GPIO1 + 0x08)) #define GPIO_PFCON (*(volatile unsigned long *)(APB0_GPIO1 + 0x0C)) +/* undocumented, kept over a reset: the OF leaves its boot request here */ +#define GPIO1_BOOTMODE (*(volatile unsigned long *)(APB0_GPIO1 + 0x1C)) #define GPIO1_TEST (*(volatile unsigned long *)(APB0_GPIO1 + 0x20)) #define GPIO_IEE (*(volatile unsigned long *)(APB0_GPIO1 + 0x24)) From 1669e49cd7c947fe4c1d9fcfa34fe415ee5e6251 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 7 Oct 2026 18:33:01 +0200 Subject: [PATCH 384/408] rk27xx: debug menu switch to show NAND SYS over USB The NAND's SYS volume holds the bootloader's firmware and is hidden unless a build defines HAVE_RK27XX_NAND_SYS. Getting at it, e.g. to replace BASE.RKW, needed a special build. Add "NAND SYS on next USB" to the debug menu. While it is set, the next USB connection shows SYS through the USER drive, as "NAND SYS", and the switch clears when that connection ends. It is decided once per connection, so setting it while connected waits for the next one, and it is held in RAM only. Writes still need FTL_ALLOW_WRITE. Co-Authored-By: Claude Opus 5.5 Change-Id: Idda3f1c5e00bea511618021397a6cb3bb0be7361 --- apps/debug_menu.c | 21 +++ firmware/target/arm/rk27xx/ata-nand-rk27xx.c | 6 +- firmware/target/arm/rk27xx/ftl-rk27xx.c | 148 +++++++++++++------ firmware/target/arm/rk27xx/ftl-target.h | 14 ++ 4 files changed, 135 insertions(+), 54 deletions(-) diff --git a/apps/debug_menu.c b/apps/debug_menu.c index c408397aca..c98d54ad64 100644 --- a/apps/debug_menu.c +++ b/apps/debug_menu.c @@ -2910,6 +2910,24 @@ static bool dbg_ftl_scheme(void) } #endif +#if (CONFIG_PLATFORM & PLATFORM_NATIVE) && defined(CONFIG_NAND) \ + && (CONFIG_NAND == NAND_RK27XX) \ + && (CONFIG_STORAGE & STORAGE_NAND) +#include "ftl-target.h" +#endif + +#ifdef FTL_SYS_ON_USB +/* The NAND's hidden SYS volume, shown to a USB host in place of USER for + * one connection */ +static bool dbg_nand_sys_usb(void) +{ + ftl_set_sys_on_usb(!ftl_get_sys_on_usb()); + splashf(HZ, "NAND SYS on next USB %sabled", + ftl_get_sys_on_usb() ? "en" : "dis"); + return false; +} +#endif + /****** The menu *********/ static const struct { unsigned char *desc; /* string or ID */ @@ -2967,6 +2985,9 @@ static const struct { && !(CONFIG_STORAGE & STORAGE_NAND) { "View FTL scheme", dbg_ftl_scheme }, #endif +#ifdef FTL_SYS_ON_USB + { "NAND SYS on next USB", dbg_nand_sys_usb }, +#endif #if (CONFIG_PLATFORM & PLATFORM_NATIVE) { "View partitions", dbg_partitions }, #endif diff --git a/firmware/target/arm/rk27xx/ata-nand-rk27xx.c b/firmware/target/arm/rk27xx/ata-nand-rk27xx.c index 84f3c02fdd..d8a3fbba12 100644 --- a/firmware/target/arm/rk27xx/ata-nand-rk27xx.c +++ b/firmware/target/arm/rk27xx/ata-nand-rk27xx.c @@ -83,11 +83,7 @@ void nand_get_info(IF_MD(int drive,) struct storage_info *info) (*info).sector_size = SECTOR_SIZE; (*info).num_sectors = ftl_get_sectors(d); (*info).vendor = "Rockchip"; -#ifdef HAVE_RK27XX_NAND_SYS - (*info).product = (d == FTL_DRIVE_SYS) ? "NAND SYS" : "NAND USER"; -#else - (*info).product = "NAND USER"; -#endif + (*info).product = ftl_drive_is_sys(d) ? "NAND SYS" : "NAND USER"; (*info).revision = "1.0"; } diff --git a/firmware/target/arm/rk27xx/ftl-rk27xx.c b/firmware/target/arm/rk27xx/ftl-rk27xx.c index ce3d4ae5d9..fcf1ffea89 100644 --- a/firmware/target/arm/rk27xx/ftl-rk27xx.c +++ b/firmware/target/arm/rk27xx/ftl-rk27xx.c @@ -30,6 +30,9 @@ #include "ftl-target.h" #include "nand-target.h" #include "flash-rk27xx.h" +#ifdef FTL_SYS_ON_USB +#include "usb.h" +#endif #if CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_A #include "ftl-scheme-a.h" @@ -65,13 +68,73 @@ struct idb_info static bool ftl_mounted = false; -#if CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_B +/* The volumes behind the drives. Both are known to the FTL even when SYS + * is not a drive. */ +#if CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_A +#define VOL_SYS FTL_A_VOL_SYS +#define VOL_USER FTL_A_VOL_USER +#else +#define VOL_SYS 0 +#define VOL_USER 1 +#define NUM_VOLS 2 + /* Scheme B's logical space holds the volumes back to back: SYS, the system * data area, then USER to the end */ -static uint32_t vol_base[FTL_NUM_DRIVES]; -static uint32_t vol_size[FTL_NUM_DRIVES]; +static uint32_t vol_base[NUM_VOLS]; +static uint32_t vol_size[NUM_VOLS]; #endif +#ifdef FTL_SYS_ON_USB +/* Asked for in the debug menu: show SYS through the USER drive while a USB + * host has the storage. Decided once per connection, so asking during one + * waits for the next, and used up when that connection ends. */ +static bool sys_requested = false; +static enum { USB_VIEW_NONE, USB_VIEW_USER, USB_VIEW_SYS } usb_view; + +static bool sys_on_usb(void) +{ + if (!usb_exclusive_storage()) + { + if (usb_view == USB_VIEW_SYS) + { + sys_requested = false; + } + usb_view = USB_VIEW_NONE; + } + else if (usb_view == USB_VIEW_NONE) + { + usb_view = sys_requested ? USB_VIEW_SYS : USB_VIEW_USER; + } + return usb_view == USB_VIEW_SYS; +} + +void ftl_set_sys_on_usb(bool on) +{ + sys_requested = on; +} + +bool ftl_get_sys_on_usb(void) +{ + return sys_requested; +} +#endif + +static int drive_volume(int drive) +{ +#ifdef HAVE_RK27XX_NAND_SYS + return (drive == FTL_DRIVE_SYS) ? VOL_SYS : VOL_USER; +#else + (void)drive; +#ifdef FTL_SYS_ON_USB + if (sys_on_usb()) + { + return VOL_SYS; + } +#endif + return VOL_USER; +#endif +} + /* ID block 1, the sector after ID block 0: * * +0 uint16_t LE raw blocks of the boot area @@ -179,12 +242,10 @@ static uint32_t mount_scheme(const struct idb_info *idb) } else { -#ifdef HAVE_RK27XX_NAND_SYS - vol_base[FTL_DRIVE_SYS] = 0; - vol_size[FTL_DRIVE_SYS] = idb->sys_sectors; -#endif - vol_base[FTL_DRIVE_USER] = user_base; - vol_size[FTL_DRIVE_USER] = ftl_b_capacity() - user_base; + vol_base[VOL_SYS] = 0; + vol_size[VOL_SYS] = idb->sys_sectors; + vol_base[VOL_USER] = user_base; + vol_size[VOL_USER] = ftl_b_capacity() - user_base; } return ret; } @@ -219,81 +280,65 @@ static bool drive_valid(int drive) } #if CONFIG_RK27XX_FTL == RK27XX_FTL_SCHEME_A -/* The FTL volume behind a drive. SYS is reachable only when exposed. */ -static int ftl_volume(int drive) +static uint32_t vol_sectors(int vol) { - int volume = FTL_A_VOL_USER; - -#ifdef HAVE_RK27XX_NAND_SYS - if (drive == FTL_DRIVE_SYS) - { - volume = FTL_A_VOL_SYS; - } -#else - (void)drive; -#endif - return volume; + return ftl_a_capacity(vol); } -static uint32_t drive_sectors(int drive) +static int vol_read(int vol, uint32_t sector, uint32_t count, void *buffer) { - return ftl_a_capacity(ftl_volume(drive)); -} - -static int drive_read(int drive, uint32_t sector, uint32_t count, void *buffer) -{ - return ftl_a_read(ftl_volume(drive), sector, buffer, count); + return ftl_a_read(vol, sector, buffer, count); } #ifdef FTL_ALLOW_WRITE -static int drive_write(int drive, uint32_t sector, uint32_t count, - const void *buffer) +static int vol_write(int vol, uint32_t sector, uint32_t count, + const void *buffer) { - return ftl_a_write(ftl_volume(drive), sector, buffer, count); + return ftl_a_write(vol, sector, buffer, count); } #endif -static void drive_sync(void) +static void vol_sync(void) { ftl_a_sync(); } #else -static uint32_t drive_sectors(int drive) +static uint32_t vol_sectors(int vol) { - return vol_size[drive]; + return vol_size[vol]; } -static bool in_volume(int drive, uint32_t sector, uint32_t count) +static bool in_volume(int vol, uint32_t sector, uint32_t count) { - return sector < vol_size[drive] && count <= vol_size[drive] - sector; + return sector < vol_size[vol] && count <= vol_size[vol] - sector; } -static int drive_read(int drive, uint32_t sector, uint32_t count, void *buffer) +static int vol_read(int vol, uint32_t sector, uint32_t count, void *buffer) { int ret = 1; - if (in_volume(drive, sector, count)) + if (in_volume(vol, sector, count)) { - ret = ftl_b_read(vol_base[drive] + sector, buffer, count); + ret = ftl_b_read(vol_base[vol] + sector, buffer, count); } return ret; } #ifdef FTL_ALLOW_WRITE -static int drive_write(int drive, uint32_t sector, uint32_t count, - const void *buffer) +static int vol_write(int vol, uint32_t sector, uint32_t count, + const void *buffer) { int ret = 1; - if (in_volume(drive, sector, count)) + if (in_volume(vol, sector, count)) { - ret = ftl_b_write(vol_base[drive] + sector, buffer, count); + ret = ftl_b_write(vol_base[vol] + sector, buffer, count); } return ret; } #endif -static void drive_sync(void) +static void vol_sync(void) { ftl_b_sync(); } @@ -305,18 +350,23 @@ uint32_t ftl_get_sectors(int drive) if (drive_valid(drive)) { - sectors = drive_sectors(drive); + sectors = vol_sectors(drive_volume(drive)); } return sectors; } +bool ftl_drive_is_sys(int drive) +{ + return drive_valid(drive) && drive_volume(drive) == VOL_SYS; +} + uint32_t ftl_read(int drive, uint32_t sector, uint32_t count, void *buffer) { uint32_t ret = 1; if (drive_valid(drive)) { - ret = drive_read(drive, sector, count, buffer) ? 2 : 0; + ret = vol_read(drive_volume(drive), sector, count, buffer) ? 2 : 0; } return ret; } @@ -329,7 +379,7 @@ uint32_t ftl_write(int drive, uint32_t sector, uint32_t count, #ifdef FTL_ALLOW_WRITE if (drive_valid(drive)) { - ret = drive_write(drive, sector, count, buffer) ? 2 : 0; + ret = vol_write(drive_volume(drive), sector, count, buffer) ? 2 : 0; } #else /* refuse rather than pretend: a silent success would let the filesystem @@ -343,7 +393,7 @@ uint32_t ftl_sync(void) { if (ftl_mounted) { - drive_sync(); + vol_sync(); } flash_sync(); return 0; diff --git a/firmware/target/arm/rk27xx/ftl-target.h b/firmware/target/arm/rk27xx/ftl-target.h index d2dc6f86b6..fde3990a3d 100644 --- a/firmware/target/arm/rk27xx/ftl-target.h +++ b/firmware/target/arm/rk27xx/ftl-target.h @@ -23,6 +23,7 @@ #include "config.h" #include "inttypes.h" +#include /* The drives the NAND presents: USER, and SYS only when the target exposes * it - see HAVE_RK27XX_NAND_SYS in the target config. */ @@ -35,6 +36,16 @@ #define FTL_NUM_DRIVES 1 #endif +#if !defined(HAVE_RK27XX_NAND_SYS) && !defined(BOOTLOADER) \ + && defined(USB_ENABLE_STORAGE) +/* Without SYS as a drive of its own, the debug menu can ask for it to be + * shown to a USB host in place of USER, for one connection: the next one, + * or the current one if none has started. Kept in RAM only. */ +#define FTL_SYS_ON_USB +void ftl_set_sys_on_usb(bool on); +bool ftl_get_sys_on_usb(void); +#endif + uint32_t ftl_init(void); uint32_t ftl_read(int drive, uint32_t sector, uint32_t count, void* buffer); uint32_t ftl_write(int drive, uint32_t sector, uint32_t count, @@ -45,5 +56,8 @@ uint32_t ftl_sync(void); * own tables, not from raw block geometry - see ftl-rk27xx.c. */ uint32_t ftl_get_sectors(int drive); +/* Whether a drive shows the SYS volume right now */ +bool ftl_drive_is_sys(int drive); + #endif From 776fe2ad9cb41a6fa541c92aacb1389c99319370 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 7 Oct 2026 22:20:18 +0200 Subject: [PATCH 385/408] rk27xx: boot Rockbox on USB wake when it can write the NAND The bootloader starts the OF when the player is woken by plugging in USB, because the OF was the only firmware that could give the host write access to the internal flash. A build with FTL_ALLOW_WRITE can do that itself, so boot Rockbox there and leave the OF to a held key. Builds without it keep the old behaviour. Co-Authored-By: Claude Opus 5.5 Change-Id: I74af2f8c56436a3f7c9ad97d1b1638469807f0dc --- bootloader/rk27xx.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bootloader/rk27xx.c b/bootloader/rk27xx.c index db4fd675ac..096555bfbf 100644 --- a/bootloader/rk27xx.c +++ b/bootloader/rk27xx.c @@ -135,9 +135,13 @@ void main(void) if ((btn & ~POWEROFF_BUTTON)) boot = of; - /* if we are woken up by USB insert boot into OF */ +#ifndef FTL_ALLOW_WRITE + /* if we are woken up by USB insert boot into OF: only it can give + * the host write access to the NAND + */ if (DEV_INFO & (1<<20)) boot = of; +#endif /* The OF asks for a mode before it resets itself, e.g. at the end of * a firmware update, and the cell survives the reset. Rockbox never From da4c218046f8eb871d8772ed9f2b7316e3771917 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 7 Oct 2026 11:50:42 +0200 Subject: [PATCH 386/408] skin_engine: fix the text of five recording tags - %Rf printed the sample rate with "%lu.%1lu", a field width rather than a precision: 44100 Hz came out as "44.100", 22050 Hz as "22.50". It is now in kHz without trailing zeros: "44.1", "22.05", "48". - %Re switched on the format setting plus one, so its text named the next format: "aiff" for WAV up to nothing for MP3. It now names the format set, MP3 as "mp3" like the others. - %Rb printed the index of the MP3 bitrate setting, "12" for 128 kbps. It now prints the bitrate in kbps. Dead code from 2009 goes. - %Rm was true for stereo: rec_channels is 0 for stereo. The classic status bar has shown the mono icon for stereo recordings since 2009. - %Rn counted the minutes on past 59, so %Rh:%Rn:%Rs showed 01:61:05 after an hour. It now counts within the hour, as %Rs does. As conditionals %Rf, %Re and %Rb keep their values, so the classic status bar's sample rate, format and bitrate icons are unchanged. Co-Authored-By: Claude Opus 5.5 Change-Id: Id9fb5c0b71fc0a44c5c7b3239a74397cb71090bf --- apps/gui/skin_engine/skin_tokens.c | 86 +++++++++--------------------- 1 file changed, 24 insertions(+), 62 deletions(-) diff --git a/apps/gui/skin_engine/skin_tokens.c b/apps/gui/skin_engine/skin_tokens.c index ecf2a9a4fc..c215ca4046 100644 --- a/apps/gui/skin_engine/skin_tokens.c +++ b/apps/gui/skin_engine/skin_tokens.c @@ -1841,16 +1841,24 @@ const char *get_token_value(struct gui_wps *gwps, numeric_ret = 12; break;) } - snprintf(buf, buf_size, "%lu.%1lu", samprk/1000,samprk%1000); + /* in kHz, without trailing zeros: 44.1, 22.05, 48 */ + snprintf(buf, buf_size, "%lu.%03lu", samprk / 1000, samprk % 1000); + { + char *end = buf + strlen(buf) - 1; + while (*end == '0') + *end-- = '\0'; + if (*end == '.') + *end = '\0'; + } numeric_buf = buf; goto gtv_ret_numeric_tag_info; } case SKIN_TOKEN_REC_ENCODER: { - int rec_format = global_settings.rec_format+1; /* WAV, AIFF, WV, MPEG */ + /* as a conditional 1 to 4: WAV, AIFF, WV, MPEG */ if (intval) - *intval = rec_format; - switch (rec_format) + *intval = global_settings.rec_format + 1; + switch (global_settings.rec_format) { case REC_FORMAT_PCM_WAV: return "wav"; @@ -1859,74 +1867,28 @@ const char *get_token_value(struct gui_wps *gwps, case REC_FORMAT_WAVPACK: return "wv"; case REC_FORMAT_MPA_L3: - return "MP3"; + return "mp3"; default: return NULL; } break; } case SKIN_TOKEN_REC_BITRATE: - if (global_settings.rec_format == REC_FORMAT_MPA_L3) + /* MP3 only: the bitrate in kbps; as a conditional, the index of + * the setting, 1 for 8 kbps up to 18 for 320 kbps */ + if (global_settings.rec_format == REC_FORMAT_MPA_L3 && + global_settings.mp3_enc_config.bitrate < MP3_ENC_NUM_BITR) { - #if 0 /* FIXME: I dont know if this is needed? */ - switch (1< Date: Wed, 7 Oct 2026 17:33:46 -0400 Subject: [PATCH 387/408] build: Only simulator builds get a 'make install' target Change-Id: I7ab3f698fba4950cf20c9448ca0f5cca00c8c100 --- tools/root.make | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/root.make b/tools/root.make index d2ff41eab4..eb574fcabf 100644 --- a/tools/root.make +++ b/tools/root.make @@ -426,8 +426,8 @@ talkclips-force: talkclips endif -ifeq (,$(findstring android, $(APP_TYPE))) - +# only simulators get a 'make install' +ifneq (,$(findstring sdl,$(APP_TYPE))) simext1: $(SILENT)mkdir -p $@ From 7ba413d87d7687addba683cce33f7a95d85e5908 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Wed, 7 Oct 2026 17:32:29 -0400 Subject: [PATCH 388/408] checkwps: Delete incorrect dependency It was accidently left in from an older iteration of 9d3d2c6e6 Change-Id: Ie911b2eb088eb967a73c39585cb4d8b69eef9203 --- tools/checkwps/checkwps.make | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/checkwps/checkwps.make b/tools/checkwps/checkwps.make index 86384dee7c..a337154f86 100644 --- a/tools/checkwps/checkwps.make +++ b/tools/checkwps/checkwps.make @@ -47,7 +47,7 @@ $(BUILDDIR)/fontbundle.h: $(ROOTDIR)/fonts/*bdf $(SILENT)echo " NULL, " >> $@ $(SILENT)echo "};" >> $@ -$(BUILDDIR)/themeassets.h: $(ROOTDIR)/firmware/export/config/$(MODELNAME).h $(ROOTDIR)/wps/WPSLIST +$(BUILDDIR)/themeassets.h: $(ROOTDIR)/wps/WPSLIST @echo THEMEASSETS $(SILENT)rm -Rf $(BUILDDIR)/.rockbox $(SILENT)mkdir $(BUILDDIR)/.rockbox From a3227cadb5c6dde27f8d1ad187e319ea0b2fc21e Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 7 Oct 2026 11:51:07 +0200 Subject: [PATCH 389/408] skin_engine: more tags for the recording screen What the built-in recording screen shows and the skin tags did not: - %RS size of the current recording, as "1.5MB" - %RP seconds in the pre-record buffer, empty unless pre-recording - %Rc clips counted by the peak meter - %Rt trigger state by name; as a conditional off, ready, steady, go, postrec, retrig, continue - %Rw recording warnings in hex, empty while there are none - %Ri recording source by name; as a conditional the same on every target: mic, line in, digital, FM radio - %Rg gain of the source in dB, the left channel's for line in and FM The manual gets a Recording section covering these and the recording tags that were there, none of which it described. Co-Authored-By: Claude Opus 5.5 Change-Id: I889f95fd472c472c2589393a6c77eff4a4fd5c8f --- apps/gui/skin_engine/skin_tokens.c | 100 +++++++++++++++++++++++++++++ lib/skin_parser/tag_table.c | 7 ++ lib/skin_parser/tag_table.h | 7 ++ manual/appendix/wps_tags.tex | 33 ++++++++++ 4 files changed, 147 insertions(+) diff --git a/apps/gui/skin_engine/skin_tokens.c b/apps/gui/skin_engine/skin_tokens.c index c215ca4046..50faaa7ec1 100644 --- a/apps/gui/skin_engine/skin_tokens.c +++ b/apps/gui/skin_engine/skin_tokens.c @@ -1917,6 +1917,106 @@ const char *get_token_value(struct gui_wps *gwps, goto gtv_ret_numeric_tag_info; } + /* bytes recorded to the current file, as "1.5MB" */ + case SKIN_TOKEN_REC_SIZE: + return output_dyn_value(buf, buf_size, audio_num_recorded_bytes(), + byte_units, 4, true); + + /* seconds in the pre-record buffer; nothing unless pre-recording */ + case SKIN_TOKEN_REC_PRERECORD: + if (!(audio_status() & AUDIO_STATUS_PRERECORD)) + return NULL; + numeric_ret = audio_prerecorded_time() / HZ; + itoa_buf(buf, buf_size, numeric_ret); + numeric_buf = buf; + goto gtv_ret_numeric_tag_info; + + case SKIN_TOKEN_REC_CLIPCOUNT: + numeric_ret = pm_get_clipcount(); + itoa_buf(buf, buf_size, numeric_ret); + numeric_buf = buf; + goto gtv_ret_numeric_tag_info; + + /* trigger state; as a conditional: off, ready, steady, go, + * post-record, retrigger, continue */ + case SKIN_TOKEN_REC_TRIGGER: + { + static const char * const trig_name[] = + { "off", "ready", "steady", "go", "postrec", "retrig", + "continue" }; + int trig = peak_meter_trigger_status(); + + if (trig < 0 || trig >= (int)ARRAYLEN(trig_name)) + return NULL; + numeric_ret = trig + 1; + numeric_buf = (char *)trig_name[trig]; + goto gtv_ret_numeric_tag_info; + } + + /* recording warnings, in hex; nothing while there are none */ + case SKIN_TOKEN_REC_WARNING: + if (!(audio_status() & AUDIO_STATUS_WARNING)) + return NULL; + snprintf(buf, buf_size, "%08lX", + (unsigned long)pcm_rec_get_warnings()); + return buf; + + /* input source; as a conditional, the same on every target: mic, + * line in, digital, FM radio */ + case SKIN_TOKEN_REC_SOURCE: + switch (global_settings.rec_source) + { +#ifdef HAVE_MIC_REC + case AUDIO_SRC_MIC: + numeric_ret = 1; + numeric_buf = (char *)str(LANG_RECORDING_SRC_MIC); + break; +#endif +#ifdef HAVE_LINE_REC + case AUDIO_SRC_LINEIN: + numeric_ret = 2; + numeric_buf = (char *)str(LANG_LINE_IN); + break; +#endif +#ifdef HAVE_SPDIF_REC + case AUDIO_SRC_SPDIF: + numeric_ret = 3; + numeric_buf = (char *)str(LANG_RECORDING_SRC_DIGITAL); + break; +#endif +#ifdef HAVE_FMRADIO_REC + case AUDIO_SRC_FMRADIO: + numeric_ret = 4; + numeric_buf = (char *)str(LANG_FM_RADIO); + break; +#endif + default: + return NULL; + } + goto gtv_ret_numeric_tag_info; + + /* gain of the current source in dB - the left channel's for line + * in and FM radio; nothing for a source without one */ + case SKIN_TOKEN_REC_GAIN: + switch (global_settings.rec_source) + { +#ifdef HAVE_MIC_REC + case AUDIO_SRC_MIC: + format_sound_value_ex(buf, buf_size, SOUND_MIC_GAIN, + global_settings.rec_mic_gain, true); + return buf; +#endif +#if defined(HAVE_LINE_REC) || defined(HAVE_FMRADIO_REC) + HAVE_LINE_REC_(case AUDIO_SRC_LINEIN:) + HAVE_FMRADIO_REC_(case AUDIO_SRC_FMRADIO:) + format_sound_value_ex(buf, buf_size, SOUND_LEFT_GAIN, + global_settings.rec_left_gain, true); + return buf; +#endif + default: + return NULL; + } + #endif /* HAVE_RECORDING */ case SKIN_TOKEN_CURRENT_SCREEN: diff --git a/lib/skin_parser/tag_table.c b/lib/skin_parser/tag_table.c index a13e58a316..2e74c440b3 100644 --- a/lib/skin_parser/tag_table.c +++ b/lib/skin_parser/tag_table.c @@ -252,6 +252,13 @@ static const struct tag_info legal_tags[] = TAG(SKIN_TOKEN_REC_SECONDS, "Rs" , "", SKIN_REFRESH_DYNAMIC), TAG(SKIN_TOKEN_REC_MINUTES, "Rn" , "", SKIN_REFRESH_DYNAMIC), TAG(SKIN_TOKEN_REC_HOURS, "Rh" , "", SKIN_REFRESH_DYNAMIC), + TAG(SKIN_TOKEN_REC_SIZE, "RS" , "", SKIN_REFRESH_DYNAMIC), + TAG(SKIN_TOKEN_REC_PRERECORD, "RP" , "", SKIN_REFRESH_DYNAMIC), + TAG(SKIN_TOKEN_REC_CLIPCOUNT, "Rc" , "", SKIN_REFRESH_DYNAMIC), + TAG(SKIN_TOKEN_REC_TRIGGER, "Rt" , "", SKIN_REFRESH_DYNAMIC), + TAG(SKIN_TOKEN_REC_WARNING, "Rw" , "", SKIN_REFRESH_DYNAMIC), + TAG(SKIN_TOKEN_REC_SOURCE, "Ri" , "", SKIN_REFRESH_DYNAMIC), + TAG(SKIN_TOKEN_REC_GAIN, "Rg" , "", SKIN_REFRESH_DYNAMIC), /* Skin variables */ TAG(SKIN_TOKEN_VAR_SET, "vs", "SSi|I", SKIN_REFRESH_DYNAMIC), diff --git a/lib/skin_parser/tag_table.h b/lib/skin_parser/tag_table.h index eb2d409487..92647c1bdd 100644 --- a/lib/skin_parser/tag_table.h +++ b/lib/skin_parser/tag_table.h @@ -276,6 +276,13 @@ enum skin_token_type { SKIN_TOKEN_REC_SECONDS, SKIN_TOKEN_REC_MINUTES, SKIN_TOKEN_REC_HOURS, + SKIN_TOKEN_REC_SIZE, + SKIN_TOKEN_REC_PRERECORD, + SKIN_TOKEN_REC_CLIPCOUNT, + SKIN_TOKEN_REC_TRIGGER, + SKIN_TOKEN_REC_WARNING, + SKIN_TOKEN_REC_SOURCE, + SKIN_TOKEN_REC_GAIN, /* Radio Tokens */ diff --git a/manual/appendix/wps_tags.tex b/manual/appendix/wps_tags.tex index 121bb7a0cf..ad56890652 100644 --- a/manual/appendix/wps_tags.tex +++ b/manual/appendix/wps_tags.tex @@ -515,6 +515,39 @@ Examples: For general information where to put album art see \reference{ref:album_art}. } +\opt{recording}{ +\section{Recording} +These tags show the state of recording, in any skin. + \begin{tagmap} + \config{\%Rp} & Can this \dap{} record?\\ + \config{\%Rr} & Is a recording in progress?\\ + \config{\%Rh} & Hours of the current recording.\\ + \config{\%Rn} & Minutes of the current recording, 00 to 59.\\ + \config{\%Rs} & Seconds of the current recording, 00 to 59.\\ + \config{\%RS} & Size of the current recording, e.g. ``1.5MB''.\\ + \config{\%RP} & Seconds in the pre-record buffer; empty unless + pre-recording.\\ + \config{\%Re} & Recording format: wav, aiff, wv or mp3. As a conditional: + \config{\%?Re}.\\ + \config{\%Rf} & Sample rate in kHz, e.g. ``44.1''. As a conditional, 1 to + 12: 96, 88.2, 64, 48, 44.1, 32, 24, 22.05, 16, 12, 11.025 and 8~kHz.\\ + \config{\%Rb} & MP3 bitrate in kbps; empty for the other formats. As a + conditional, 1 to 18 for 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, + 144, 160, 192, 224, 256 and 320~kbps.\\ + \config{\%Rm} & Is the recording mono?\\ + \config{\%Ri} & Recording source, by name. As a conditional, the same on + every \dap{}: \config{\%?Ri}.\\ + \config{\%Rg} & Gain of the recording source in dB -- for line in and + FM radio the left channel's. Empty for a source without a gain.\\ + \config{\%Rc} & Number of clips the peak meter has counted.\\ + \config{\%Rt} & Trigger state: off, ready, steady, go, postrec, retrig or + continue. As a conditional: + \config{\%?Rt}.\\ + \config{\%Rw} & Recording warnings, as a hexadecimal number; empty while + there are none.\\ + \end{tagmap} +} + \opt{radio}{ \section{FM Radio} \begin{tagmap} From 303c534022029eacee5780f2780429e58b5cf46f Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Wed, 7 Oct 2026 22:48:55 -0400 Subject: [PATCH 390/408] test_disk: ask which disk to test The tests always ran on HOME_DIR, so a card could not be tested, and the internal storage could not be left alone. Where there is more than one volume the plugin now asks which to test when it starts: HOME_DIR as before, or any volume the root directory lists. "Select disk" in its menu changes it. The test directory, the test file and the log are all on the disk chosen, where the log used to go to HOME_DIR, so that testing a card writes nothing to the internal storage. The log names the disk. Co-Authored-By: Claude Opus 5.5 Change-Id: Ib243fdd32fa205e00bc2b39d776e0fc8a420a1f0 --- apps/plugins/test_disk.c | 161 +++++++++++++++++++++++++++++++-------- 1 file changed, 130 insertions(+), 31 deletions(-) diff --git a/apps/plugins/test_disk.c b/apps/plugins/test_disk.c index d53f641b85..7fb9114974 100644 --- a/apps/plugins/test_disk.c +++ b/apps/plugins/test_disk.c @@ -23,8 +23,7 @@ -#define TESTBASEDIR HOME_DIR "/__TEST__" -#define TEST_FILE TESTBASEDIR "/test_disk.tmp" +#define TESTDIRNAME "__TEST__" #define FRND_SEED 0x78C3 /* arbirary */ #if (CONFIG_STORAGE & STORAGE_MMC) @@ -42,7 +41,20 @@ static int line = 0; static int max_line = 0; static int log_fd; static char logfilename[MAX_PATH]; -static const char testbasedir[] = TESTBASEDIR; + +/* The disk under test: HOME_DIR, or where there are several volumes the + one chosen from the menu. Its test directory, test file and log are all + on it, so nothing is written anywhere else. */ +#ifdef HAVE_MULTIVOLUME +#define MAX_DISKS (NUM_VOLUMES + 1) +#else +#define MAX_DISKS 1 +#endif +static char disks[MAX_DISKS][24]; +static int ndisks; +static int disk; +static char testbasedir[MAX_PATH]; +static char testfile[MAX_PATH]; static void mem_fill_frnd(unsigned char *addr, int len) { @@ -82,8 +94,8 @@ static bool log_init(void) rb->lcd_clear_display(); rb->lcd_update(); - rb->create_numbered_filename(logfilename, HOME_DIR, "test_disk_log_", ".txt", - 2 IF_CNFN_NUM_(, NULL)); + rb->create_numbered_filename(logfilename, disks[disk], "test_disk_log_", + ".txt", 2 IF_CNFN_NUM_(, NULL)); log_fd = rb->open(logfilename, O_RDWR|O_CREAT|O_TRUNC, 0666); return log_fd >= 0; } @@ -105,6 +117,84 @@ static void log_close(void) rb->close(log_fd); } +/* The name of the i'th of the many small files the speed test makes. */ +static void tmp_name(char *buf, size_t size, int i) +{ + rb->snprintf(buf, size, "%s/%08x.tmp", testbasedir, i); +} + +/* The disks there are to test: HOME_DIR, and each volume the root lists. */ +static void find_disks(void) +{ + rb->strlcpy(disks[0], HOME_DIR, sizeof disks[0]); + ndisks = 1; +#ifdef HAVE_MULTIVOLUME + { + DIR *dir = rb->opendir(PATH_ROOTSTR); + struct dirent *entry; + + if (dir == NULL) + return; + while (ndisks < MAX_DISKS && (entry = rb->readdir(dir)) != NULL) + { + if (!(rb->dir_get_info(dir, entry).attribute & ATTR_VOLUME)) + continue; + rb->snprintf(disks[ndisks], sizeof disks[0], PATH_ROOTSTR "%s", + entry->d_name); + ndisks++; + } + rb->closedir(dir); + } +#endif +} + +/* Make disk n the one under test. The test directory on the one before + is removed and one made here. */ +static bool use_disk(int n) +{ + DIR *dir; + + if (testbasedir[0]) + rb->rmdir(testbasedir); + disk = n; + rb->snprintf(testbasedir, sizeof testbasedir, "%s/" TESTDIRNAME, + disks[disk]); + rb->snprintf(testfile, sizeof testfile, "%s/test_disk.tmp", testbasedir); + + if ((dir = rb->opendir(testbasedir)) == NULL) + { + if (rb->mkdir(testbasedir) < 0) + { + rb->splashf(HZ*2, "Can't create %s", testbasedir); + testbasedir[0] = '\0'; + return false; + } + } + else + { + rb->closedir(dir); + } + return true; +} + +#ifdef HAVE_MULTIVOLUME +/* Ask which disk to test. */ +static bool select_disk(void) +{ + struct opt_items names[MAX_DISKS]; + int n = disk; + int i; + + for (i = 0; i < ndisks; i++) + { + names[i].string = disks[i]; + names[i].voice_id = -1; + } + rb->set_option("Disk to test", &n, RB_INT, names, ndisks, NULL); + return use_disk(n); +} +#endif + static bool test_fs(void) { unsigned char text_buf[32]; @@ -113,6 +203,7 @@ static bool test_fs(void) log_init(); log_text("test_disk WRITE&VERIFY", true); + log_text(disks[disk], true); #if (CONFIG_PLATFORM & PLATFORM_NATIVE) rb->snprintf(text_buf, sizeof(text_buf), "CPU clock: %ld Hz", *rb->cpu_frequency); @@ -122,7 +213,7 @@ static bool test_fs(void) rb->snprintf(text_buf, sizeof text_buf, "Data size: %dKB", (TEST_SIZE>>10)); log_text(text_buf, true); - fd = rb->creat(TEST_FILE, 0666); + fd = rb->creat(testfile, 0666); if (fd < 0) { rb->splashf(HZ, "creat() failed: %d", fd); @@ -152,7 +243,7 @@ static bool test_fs(void) } rb->close(fd); - fd = rb->open(TEST_FILE, O_RDONLY); + fd = rb->open(testfile, O_RDONLY); if (fd < 0) { rb->splashf(0, "open() failed: %d", fd); @@ -192,7 +283,7 @@ static bool test_fs(void) error: log_close(); - rb->remove(TEST_FILE); + rb->remove(testfile); rb->button_clear_queue(); rb->button_get(true); @@ -212,7 +303,7 @@ static bool file_speed(int chunksize, bool align) log_text("--------------------", true); /* File creation write speed */ - fd = rb->creat(TEST_FILE, 0666); + fd = rb->creat(testfile, 0666); if (fd < 0) { rb->splashf(HZ, "creat() failed: %d", fd); @@ -237,7 +328,7 @@ static bool file_speed(int chunksize, bool align) log_text(text_buf, true); /* Existing file write speed */ - fd = rb->open(TEST_FILE, O_WRONLY); + fd = rb->open(testfile, O_WRONLY); if (fd < 0) { rb->splashf(0, "open() failed: %d", fd); @@ -261,7 +352,7 @@ static bool file_speed(int chunksize, bool align) log_text(text_buf, true); /* File read speed */ - fd = rb->open(TEST_FILE, O_RDONLY); + fd = rb->open(testfile, O_RDONLY); if (fd < 0) { rb->splashf(0, "open() failed: %d", fd); @@ -283,11 +374,11 @@ static bool file_speed(int chunksize, bool align) rb->snprintf(text_buf, sizeof text_buf, "Read (%d,%c): %ld KB/s", chunksize, align ? 'A' : 'U', (25 * (filesize>>8) / time) ); log_text(text_buf, true); - rb->remove(TEST_FILE); + rb->remove(testfile); return true; error: - rb->remove(TEST_FILE); + rb->remove(testfile); return false; } @@ -303,6 +394,7 @@ static bool test_speed(void) rb->memset(audiobuf, 'T', audiobuflen); log_init(); log_text("test_disk SPEED TEST", true); + log_text(disks[disk], true); #if (CONFIG_PLATFORM & PLATFORM_NATIVE) rb->snprintf(text_buf, sizeof(text_buf), "CPU clock: %ld Hz", *rb->cpu_frequency); @@ -314,7 +406,7 @@ static bool test_speed(void) time = *rb->current_tick + TEST_TIME*HZ; for (i = 0; TIME_BEFORE(*rb->current_tick, time); i++) { - rb->snprintf(text_buf, sizeof(text_buf), TESTBASEDIR "/%08x.tmp", i); + tmp_name(text_buf, sizeof(text_buf), i); fd = rb->creat(text_buf, 0666); if (fd < 0) { @@ -335,7 +427,7 @@ static bool test_speed(void) { if (i >= last_file) i = 0; - rb->snprintf(text_buf, sizeof(text_buf), TESTBASEDIR "/%08x.tmp", i); + tmp_name(text_buf, sizeof(text_buf), i); fd = rb->open(text_buf, O_RDONLY); if (fd < 0) { @@ -397,7 +489,7 @@ static bool test_speed(void) time = *rb->current_tick; for (i = 0; i < last_file; i++) { - rb->snprintf(text_buf, sizeof(text_buf), TESTBASEDIR "/%08x.tmp", i); + tmp_name(text_buf, sizeof(text_buf), i); rb->remove(text_buf); } rb->snprintf(text_buf, sizeof(text_buf), "Delete: %ld files/s", @@ -420,7 +512,7 @@ static bool test_speed(void) error: for (i = 0; i < last_file; i++) { - rb->snprintf(text_buf, sizeof(text_buf), TESTBASEDIR "/%08x.tmp", i); + tmp_name(text_buf, sizeof(text_buf), i); rb->remove(text_buf); } log_text("DONE", false); @@ -434,26 +526,26 @@ static bool test_speed(void) /* this is the plugin entry point */ enum plugin_status plugin_start(const void* parameter) { +#ifdef HAVE_MULTIVOLUME + MENUITEM_STRINGLIST(menu, "Test Disk", NULL, + "Disk speed", "Write & verify", "Select disk"); +#else MENUITEM_STRINGLIST(menu, "Test Disk", NULL, "Disk speed", "Write & verify"); +#endif int selected=0; bool quit = false; - DIR *dir; (void)parameter; - if ((dir = rb->opendir(testbasedir)) == NULL) - { - if (rb->mkdir(testbasedir) < 0) - { - rb->splash(HZ*2, "Can't create test directory."); - return PLUGIN_ERROR; - } - } - else - { - rb->closedir(dir); - } + find_disks(); +#ifdef HAVE_MULTIVOLUME + if (ndisks > 1 ? !select_disk() : !use_disk(0)) + return PLUGIN_ERROR; +#else + if (!use_disk(0)) + return PLUGIN_ERROR; +#endif audiobuf = rb->plugin_get_audio_buffer(&audiobuflen); #ifdef STORAGE_WANTS_ALIGN @@ -480,6 +572,12 @@ enum plugin_status plugin_start(const void* parameter) case 1: test_fs(); break; +#ifdef HAVE_MULTIVOLUME + case 2: + if (!select_disk()) + quit = true; + break; +#endif default: quit = true; break; @@ -489,7 +587,8 @@ enum plugin_status plugin_start(const void* parameter) /* Turn on backlight timeout (revert to settings) */ backlight_use_settings(); - rb->rmdir(testbasedir); + if (testbasedir[0]) + rb->rmdir(testbasedir); return PLUGIN_OK; } From 853274937cb293633679e8f9a9b919fcc0671ae8 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Wed, 7 Oct 2026 22:55:29 -0400 Subject: [PATCH 391/408] test_disk: scroll the log when the screen is full When the text reached the bottom of the screen it started again at the top, over what was there, so the old lines showed through the new. Scroll up a line instead, as test_codec does. Co-Authored-By: Claude Opus 5.5 Change-Id: I024eda05bbcca6f1539893ea609c89f562436e2f --- apps/plugins/test_disk.c | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/plugins/test_disk.c b/apps/plugins/test_disk.c index 7fb9114974..f628195b87 100644 --- a/apps/plugins/test_disk.c +++ b/apps/plugins/test_disk.c @@ -20,6 +20,7 @@ #include "plugin.h" #include "lib/helper.h" +#include "lib/xlcd.h" @@ -39,6 +40,7 @@ static size_t audiobuflen; static unsigned short frnd_buffer; static int line = 0; static int max_line = 0; +static int line_height = 0; static int log_fd; static char logfilename[MAX_PATH]; @@ -86,10 +88,8 @@ static bool mem_cmp_frnd(unsigned char *addr, int len) static bool log_init(void) { - int h; - - rb->lcd_getstringsize("A", NULL, &h); - max_line = LCD_HEIGHT / h; + rb->lcd_getstringsize("A", NULL, &line_height); + max_line = LCD_HEIGHT / line_height; line = 0; rb->lcd_clear_display(); rb->lcd_update(); @@ -102,12 +102,17 @@ static bool log_init(void) static void log_text(char *text, bool advance) { + if (line >= max_line) + { + /* The screen is full: scroll up to make room for this line */ + xlcd_scroll_up(line_height); + line = max_line - 1; + } rb->lcd_puts(0, line, text); rb->lcd_update(); if (advance) { - if (++line >= max_line) - line = 0; + line++; rb->fdprintf(log_fd, "%s\n", text); } } From aba3fe1d57f44c26b7508b2e9298a4990b5dcc6a Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 8 Oct 2026 10:26:40 +0200 Subject: [PATCH 392/408] YP-CP3: switch the headphone amplifier pin to GPIO The headphone amplifier enable, GPIO F2, shares its pin with SDRAM address line A12 and is A12 until switched in IOMUXB. Rockbox never switched it, so driving F2 never reached the amplifier: started from the Rockbox bootloader, playback and the FM radio were near silent and distorted. They only worked when the original firmware had run first, as it switches the pin. The 16 MB of SDRAM does not use A12. Tested on a YP-CP3 booted from the Rockbox bootloader. Co-Authored-By: Claude Opus 5.5 Change-Id: I40610aaf88f06fd4831d07174b0f66a4ba32204d --- firmware/drivers/audio/wm8751.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/firmware/drivers/audio/wm8751.c b/firmware/drivers/audio/wm8751.c index 2f6da1f4e0..006e9d8b78 100644 --- a/firmware/drivers/audio/wm8751.c +++ b/firmware/drivers/audio/wm8751.c @@ -188,7 +188,10 @@ void audiohw_preinit(void) or_l((1<<5), &GPIO1_ENABLE); or_l((1<<5), &GPIO1_FUNCTION); #elif defined(SAMSUNG_YPCP3) - /* headphone amplifier enable, GPIO F2 active high: off for now */ + /* headphone amplifier enable, GPIO F2 active high: off for now. The + * pin is SDRAM A12 until switched to GPIO, which the 16 MB of SDRAM + * does not need; the original firmware switches it. */ + SCU_IOMUXB_CON |= IOMUX_GPIO_F2; GPIO_PFDR &= ~(1<<2); GPIO_PFCON |= (1<<2); #endif From be0153afbd0b0c9d6de252bd01d5ba85baaf8b5a Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 7 Oct 2026 22:58:14 +0200 Subject: [PATCH 393/408] YP-CP3: record the FM radio at +12 dB by default The radio is heard through the codec's input PGA at +12 dB, but on the recording screen the gain started at 0 dB, so a recording was much quieter than what was heard just before. Start the line gain, which the radio uses, at +12 dB. Defining the defaults also makes the line gain a saved setting on this target; before, it went back to 0 dB at every boot. Tested on a YP-CP3: switching from the FM screen to the recording screen with FM as the source keeps the same loudness. Co-Authored-By: Claude Opus 5.5 Change-Id: Id65ce25515d7143b2ca21e5ef2a110a356303380 --- firmware/export/config/samsungypcp3.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/firmware/export/config/samsungypcp3.h b/firmware/export/config/samsungypcp3.h index f3542f64f6..5b8a8d1803 100644 --- a/firmware/export/config/samsungypcp3.h +++ b/firmware/export/config/samsungypcp3.h @@ -20,6 +20,12 @@ | SAMPR_CAP_48 | SAMPR_CAP_24 | SAMPR_CAP_12 \ | SAMPR_CAP_32 | SAMPR_CAP_16 | SAMPR_CAP_8) +/* Line gain, used for the FM radio, in 0.01 dB: start at the +12 dB the + * radio is heard at when not recording (wm8751.c), so recording it is as + * loud as listening to it */ +#define DEFAULT_REC_LEFT_GAIN 1200 +#define DEFAULT_REC_RIGHT_GAIN 1200 + /* define this if you have a colour LCD */ #define HAVE_LCD_COLOR From 8036fda6f0733b3ae2f9e6815051635c572f890f Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Thu, 8 Oct 2026 13:43:21 +0200 Subject: [PATCH 394/408] rk27load: fix build flags for s1 and s2 images Change-Id: Id3ba41337fc1ac4b3764a344682147173580fd95 --- utils/rk27utils/rk27load/stage1/Makefile | 4 ++-- utils/rk27utils/rk27load/stage2/Makefile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/utils/rk27utils/rk27load/stage1/Makefile b/utils/rk27utils/rk27load/stage1/Makefile index 5291685032..a783977533 100644 --- a/utils/rk27utils/rk27load/stage1/Makefile +++ b/utils/rk27utils/rk27load/stage1/Makefile @@ -10,7 +10,7 @@ AS = $(TOOLCHAIN)as OBJCOPY = $(TOOLCHAIN)objcopy OBJDUMP = $(TOOLCHAIN)objdump -CFLAGS = -Wundef -marm -march=armv5te -nostdlib -mfpu=fpa -O0 -c +CFLAGS = -Wundef -marm -march=armv5te -nostdlib -O0 -c #ASFLAGS = -mcpu=arm926ej-s OBJS = main.o @@ -20,7 +20,7 @@ LDSCRIPT= stage1.lds #LIBS = -lgcc LIBS = LDFLAGS = -Wundef -marm -march=armv5te -T$(LDSCRIPT) -nostartfiles \ - -mfpu=fpa -nostdlib -Xlinker -Map=$(TARGET).map + -nostdlib -Xlinker -Map=$(TARGET).map all : $(TARGET).bin ls -ls $(TARGET).bin diff --git a/utils/rk27utils/rk27load/stage2/Makefile b/utils/rk27utils/rk27load/stage2/Makefile index 4b216db2f3..37bfcccfdc 100644 --- a/utils/rk27utils/rk27load/stage2/Makefile +++ b/utils/rk27utils/rk27load/stage2/Makefile @@ -10,7 +10,7 @@ AS = $(TOOLCHAIN)as OBJCOPY = $(TOOLCHAIN)objcopy OBJDUMP = $(TOOLCHAIN)objdump -CFLAGS = -Wundef -marm -march=armv5te -nostdlib -mfpu=fpa -O0 -c +CFLAGS = -Wundef -marm -march=armv5te -nostdlib -O0 -c #ASFLAGS = -mcpu=arm926ej-s OBJS = crt0.o main.o irq.o @@ -20,7 +20,7 @@ LDSCRIPT= stage2.lds #LIBS = -lgcc LIBS = LDFLAGS = -Wundef -marm -march=armv5te -T$(LDSCRIPT) -nostartfiles \ - -mfpu=fpa -nostdlib -Xlinker -Map=$(TARGET).map + -nostdlib -Xlinker -Map=$(TARGET).map all : $(TARGET).bin ls -ls $(TARGET).bin From 860582a63a600866206183a72cd6ab27994aff78 Mon Sep 17 00:00:00 2001 From: Amaury Pouly Date: Tue, 20 Feb 2018 21:58:09 +1100 Subject: [PATCH 395/408] hwstub_load: fix missing break in switch Change-Id: I7758bcbf9c0726a44c9dc0b4968f8d2d8c978874 --- utils/hwstub/tools/hwstub_load.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/hwstub/tools/hwstub_load.cpp b/utils/hwstub/tools/hwstub_load.cpp index 65ce64b05c..44ac56fda2 100644 --- a/utils/hwstub/tools/hwstub_load.cpp +++ b/utils/hwstub/tools/hwstub_load.cpp @@ -181,6 +181,7 @@ int main(int argc, char **argv) break; case 'v': verbose = true; + break; case 'e': no_load = true; break; From 2facad742490f16f769e7c9fbd6397efc2dbaf45 Mon Sep 17 00:00:00 2001 From: Amaury Pouly Date: Tue, 20 Feb 2018 21:57:43 +1100 Subject: [PATCH 396/408] hwstub tools: fix/improve usage help Change-Id: Id1afbe8adaf29887fc97c570ae33127a40a046b6 --- utils/hwstub/tools/hwstub_load.cpp | 5 ++--- utils/hwstub/tools/hwstub_shell.cpp | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/utils/hwstub/tools/hwstub_load.cpp b/utils/hwstub/tools/hwstub_load.cpp index 44ac56fda2..8db1963174 100644 --- a/utils/hwstub/tools/hwstub_load.cpp +++ b/utils/hwstub/tools/hwstub_load.cpp @@ -107,12 +107,11 @@ void usage(void) { printf("usage: hwstub_load [options] \n"); printf("options:\n"); - printf(" --help/-? Display this help\n"); + printf(" --help/-h Display this help\n"); printf(" --quiet/-q Quiet output\n"); printf(" --verbose/-v Verbose output\n"); printf(" --type/-t Override file type\n"); printf(" --dev/-d Device URI (see below)\n"); - printf(" --verbose/-v Display debug output\n"); printf(" --noload Skip loading stage and only execute the given address\n"); printf(" --noexec Skip execute stage and only load data the given address\n"); printf("file types:\n"); @@ -123,7 +122,7 @@ void usage(void) for(int i = 0; players[i].name; i++) printf(" %s", players[i].name); printf("\n"); - //hwstub::usage_uri(stdout); + hwstub::uri::print_usage(stdout, true, false); exit(1); } diff --git a/utils/hwstub/tools/hwstub_shell.cpp b/utils/hwstub/tools/hwstub_shell.cpp index 9fda72981f..4f94be08ab 100644 --- a/utils/hwstub/tools/hwstub_shell.cpp +++ b/utils/hwstub/tools/hwstub_shell.cpp @@ -1341,7 +1341,7 @@ void usage(void) printf(" --debug-rw Print read/write\n"); printf("Relative order of -e and -f commands are preserved.\n"); printf("Unless specified, the shell will load soc desc in regtools/desc/\n"); - //usage_uri(stdout); + hwstub::uri::print_usage(stdout, true, false); exit(1); } From 966c1d76505a0e7a75dbd29a642efa440b05e5e5 Mon Sep 17 00:00:00 2001 From: Amaury Pouly Date: Fri, 23 Feb 2018 22:13:55 +1100 Subject: [PATCH 397/408] hwstub: bugfix in library Change-Id: I1af4b53d872b0691025cc0efd99c1927c2d9384b --- utils/hwstub/lib/hwstub_net.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/utils/hwstub/lib/hwstub_net.cpp b/utils/hwstub/lib/hwstub_net.cpp index 89994cdf7a..6f828f81ec 100644 --- a/utils/hwstub/lib/hwstub_net.cpp +++ b/utils/hwstub/lib/hwstub_net.cpp @@ -391,7 +391,7 @@ error context::send_cmd(uint32_t cmd, uint32_t args[HWSTUB_NET_ARGS], uint8_t *s } } /* copy data if user want it */ - if(recv_data) + if(recv_data && recv_size) { if(*recv_data == nullptr) { @@ -1221,7 +1221,6 @@ error server::handle_cmd(client_state *state, uint32_t cmd, uint32_t args[HWSTUB error err = it->second->write(addr, recv_data, recv_size, !!(flags & HWSERVER_RW_ATOMIC)); if(err != error::SUCCESS) { - delete[] send_data; debug() << "[net::srv::cmd] cannot write: " << error_string(err) << "\n"; debug() << "[net::srv::cmd] <-- WRITE (error)\n"; return err; From eb39277d4a5bababbd7169af2106b396125f1c65 Mon Sep 17 00:00:00 2001 From: Amaury Pouly Date: Fri, 23 Feb 2018 22:13:19 +1100 Subject: [PATCH 398/408] hwstub: allow minor version mismatch in net layer Change-Id: I81a75bd7b9f3fd1a56e5bea521966c1847adfec7 --- utils/hwstub/include/hwstub_net.hpp | 2 ++ utils/hwstub/lib/hwstub_net.cpp | 33 ++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/utils/hwstub/include/hwstub_net.hpp b/utils/hwstub/include/hwstub_net.hpp index 72d73e83e5..f6992b5348 100644 --- a/utils/hwstub/include/hwstub_net.hpp +++ b/utils/hwstub/include/hwstub_net.hpp @@ -112,6 +112,7 @@ protected: virtual void destroy_device_list(void *ptr); virtual error create_device(ctx_dev_t dev, std::shared_ptr& hwdev); virtual bool match_device(ctx_dev_t dev, std::shared_ptr hwdev); + static inline int hwstub_ver(int major, int minor) { return major * 100 + minor; } enum class state { @@ -122,6 +123,7 @@ protected: state m_state; /* client state */ error m_error; /* error state for DEAD */ + int m_ver; /* Server revision (uses hwstub_ver()) */ }; /** Socket based net context diff --git a/utils/hwstub/lib/hwstub_net.cpp b/utils/hwstub/lib/hwstub_net.cpp index 6f828f81ec..851723f37d 100644 --- a/utils/hwstub/lib/hwstub_net.cpp +++ b/utils/hwstub/lib/hwstub_net.cpp @@ -437,14 +437,25 @@ void context::delayed_init() return; } /* check the server is running the same version */ - debug() << "[net::ctx] <-- HELLO " << ((args[0] & 0xff00) >> 8) << "." << (args[0] & 0xff) << ""; - if(args[0] != (HWSTUB_VERSION_MAJOR << 8 | HWSTUB_VERSION_MINOR)) + int serv_maj = (args[0] >> 8) & 0xff; + int serv_min = args[0] & 0xff; + debug() << "[net::ctx] <-- HELLO " << serv_maj << "." << serv_min << ""; + + m_ver = hwstub_ver(serv_maj, serv_min); + /* Major mismatch -> error */ + if(serv_maj != HWSTUB_VERSION_MAJOR) { - debug() << " (mismatch)\n"; + debug() << " (major version mismatch)\n"; m_state = state::DEAD; m_error = error::SERVER_MISMATCH; } - debug() << " (good)\n"; + else + debug() << " (good)\n"; + /* Minor mismatch -> warning */ + if(serv_min < HWSTUB_VERSION_MINOR) + debug() << "[net::ctx] Some operations might not be supported by the server.\n"; + else if(serv_min > HWSTUB_VERSION_MINOR) + debug() << "[net::ctx] Some operations might not be supported by this client.\n"; /* good, we can now send commands */ m_state = state::IDLE; } @@ -994,16 +1005,22 @@ error server::handle_cmd(client_state *state, uint32_t cmd, uint32_t args[HWSTUB /* HELLO */ if(cmd == HWSERVER_HELLO) { - debug() << "[net::srv::cmd] --> HELLO " << ((args[0] & 0xff00) >> 8) - << "." << (args[0] & 0xff); - if(args[0] != (HWSTUB_VERSION_MAJOR << 8 | HWSTUB_VERSION_MINOR)) + int cli_maj = (args[0] >> 8) & 0xff; + int cli_min = args[0] & 0xff; + debug() << "[net::srv::cmd] --> HELLO " << cli_maj << "." << cli_min; + if(cli_maj != HWSTUB_VERSION_MAJOR) { - debug() << " (mismatch)\n"; + debug() << " (major version mismatch)\n"; return error::ERROR; } debug() << " (good)\n"; debug() << "[net::srv::cmd] <-- HELLO " << HWSTUB_VERSION_MAJOR << "." << HWSTUB_VERSION_MINOR << "\n"; + /* Minor mismatch -> warning */ + if(cli_min < HWSTUB_VERSION_MINOR) + debug() << "[net::ctx] Some operations might not be supported by the client.\n"; + else if(cli_min > HWSTUB_VERSION_MINOR) + debug() << "[net::ctx] Some operations might not be supported by this server.\n"; /* send HELLO with our version */ args[0] = HWSTUB_VERSION_MAJOR << 8 | HWSTUB_VERSION_MINOR; return error::SUCCESS; From cea6fca67be099ff1a69a5e26cc8c17b7c6111a2 Mon Sep 17 00:00:00 2001 From: Amaury Pouly Date: Wed, 5 Oct 2016 14:43:05 +0100 Subject: [PATCH 399/408] hwstub: add stub gdb backend Change-Id: I1e3fe2bec3f5d926a02027c13b5056d461da2f9d --- utils/hwstub/include/hwstub_gdb.hpp | 112 +++++++++++++ utils/hwstub/include/hwstub_net.hpp | 55 +++++- utils/hwstub/lib/hwstub_gdb.cpp | 224 +++++++++++++++++++++++++ utils/hwstub/lib/hwstub_net.cpp | 250 ++++++++++++++++++---------- utils/hwstub/lib/hwstub_uri.cpp | 9 + 5 files changed, 561 insertions(+), 89 deletions(-) create mode 100644 utils/hwstub/include/hwstub_gdb.hpp create mode 100644 utils/hwstub/lib/hwstub_gdb.cpp diff --git a/utils/hwstub/include/hwstub_gdb.hpp b/utils/hwstub/include/hwstub_gdb.hpp new file mode 100644 index 0000000000..4a9da61de3 --- /dev/null +++ b/utils/hwstub/include/hwstub_gdb.hpp @@ -0,0 +1,112 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2016 by Amaury Pouly + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef __HWSTUB_GDB_HPP__ +#define __HWSTUB_GDB_HPP__ + +#include "hwstub.hpp" +#include "hwstub_net.hpp" + +namespace hwstub { +namespace gdb { + + +/** GDB context + * + * A GDB context connects to a GDB server and has only one device. */ +class context : public hwstub::context +{ + friend class device; + friend class handle; +protected: + context(int socke_fd); +public: + virtual ~context(); + /** Create a context from an existing socket */ + static std::shared_ptr create(int socket_fd); + /** Create a TCP socket context with a domain name and a port. If port is empty, + * a default port is used. */ + static std::shared_ptr create_tcp(const std::string& domain, + const std::string& port, std::string *error = nullptr); + +protected: + /** Send a message to the server. Context will always serialize calls to send() + * so there is no need to worry about concurrency issues. */ + error send(void *buffer, size_t& sz); + /** Receive a message from the server, sz is updated with the received size. + * Context will always serialize calls to recv() so there is no need to + * worry about concurrency issues. */ + error recv(void *buffer, size_t& sz); + /* NOTE ctx_dev_t is always 0 */ + virtual error fetch_device_list(std::vector& list, void*& ptr); + virtual void destroy_device_list(void *ptr); + virtual error create_device(ctx_dev_t dev, std::shared_ptr& hwdev); + virtual bool match_device(ctx_dev_t dev, std::shared_ptr hwdev); + + hwstub::net::socket_connection m_conn; +}; + +/** GDB device + * + * Device based on gdb context. */ +class device : public hwstub::device +{ + friend class context; /* for ctor */ +protected: + device(std::shared_ptr ctx); +public: + virtual ~device(); + +protected: + virtual error open_dev(std::shared_ptr& handle); + virtual bool has_multiple_open() const; +}; + +/** GDB handle + * + * Handle based on gdb device. */ +class handle : public hwstub::handle +{ + friend class device; /* for ctor */ +protected: + handle(std::shared_ptr dev); +public: + virtual ~handle(); + +protected: + virtual error read_dev(uint32_t addr, void *buf, size_t& sz, bool atomic); + virtual error write_dev(uint32_t addr, const void *buf, size_t& sz, bool atomic); + virtual error get_dev_desc(uint16_t desc, void *buf, size_t& buf_sz); + virtual error get_dev_log(void *buf, size_t& buf_sz); + virtual error exec_dev(uint32_t addr, uint16_t flags); + virtual error status() const; + virtual size_t get_buffer_size(); + +protected: + struct hwstub_version_desc_t m_desc_version; + struct hwstub_layout_desc_t m_desc_layout; + struct hwstub_target_desc_t m_desc_target; +}; + +} // namespace virt +} // namespace hwstub + +#endif /* __HWSTUB_GDB_HPP__ */ + diff --git a/utils/hwstub/include/hwstub_net.hpp b/utils/hwstub/include/hwstub_net.hpp index f6992b5348..6ea36af0eb 100644 --- a/utils/hwstub/include/hwstub_net.hpp +++ b/utils/hwstub/include/hwstub_net.hpp @@ -29,6 +29,58 @@ namespace hwstub { namespace net { +/** Net helper + * + * This class provides helpers to implement a socket based context. It abstracts + * send/recv to return sensible error codes, print debug messages, and has helpers + * to create unix and tcp sockets. + * + * NOTE: this class is NOT thread-safe */ +class socket_connection +{ +public: + socket_connection(); + ~socket_connection(); + /** Return socket file descriptor, or -1 if none */ + int get_socket_fd(); + /** Init a socket connection with an existing file descriptor. Note that the + * file descriptor will be closed when the class will be destroyed or init + * again. If a previous socket was associated with this class before, it will + * be closed. */ + void init(int socket_fd); + /** Close the socket connection now */ + void close(); + /** Create a TCP connection with a domain name and a port. + * In case of error, return -1 */ + static int create_tcp(const std::string& domain, + const std::string& port, std::string *error = nullptr); + /** Init a UNIX socket connection with a file system path (see man for details) + * In case of error, the previous connection is NOT closed and + * the function returns false, otherwise it returns true. */ + static int create_unix(const std::string& path, + std::string *error = nullptr); + /** Init a UNIX socket connection with an abstract name (see man for details) + * In case of error, the previous connection is NOT closed and + * the function returns false, otherwise it returns true. */ + static int create_unix_abstract(const std::string& path, + std::string *error = nullptr); + + /** Useful functions for network byte order conversion */ + static uint32_t to_net_order(uint32_t u); + static uint32_t from_net_order(uint32_t u); + + /** set operation timeout */ + void set_timeout(std::chrono::milliseconds ms); + + /** Send a message to the server. */ + error send(void *buffer, size_t& sz, std::ostream& debug = cnull); + /** Receive a message from the server, sz is updated with the received size. */ + error recv(void *buffer, size_t& sz, std::ostream& debug = cnull); + +protected: + int m_socketfd; /* socket file descriptor */ +}; + /** Net context * * A socket context provides access to another context through a network. This @@ -56,6 +108,7 @@ public: /** Create a UNIX socket context with an abstract name (see man for details) */ static std::shared_ptr create_unix_abstract(const std::string& path, std::string *error = nullptr); + /** Useful functions for network byte order conversion */ uint32_t to_net_order(uint32_t u); uint32_t from_net_order(uint32_t u); @@ -144,7 +197,7 @@ protected: virtual error send(void *buffer, size_t& sz); virtual error recv(void *buffer, size_t& sz); - int m_socketfd; /* socket file descriptor */ + socket_connection m_conn; }; diff --git a/utils/hwstub/lib/hwstub_gdb.cpp b/utils/hwstub/lib/hwstub_gdb.cpp new file mode 100644 index 0000000000..1c1b453f6c --- /dev/null +++ b/utils/hwstub/lib/hwstub_gdb.cpp @@ -0,0 +1,224 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2016 by Amaury Pouly + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include "hwstub_gdb.hpp" +#include +#include + +namespace hwstub { +namespace gdb { + +/** + * Context + */ +context::context(int socket_fd) +{ + m_conn.init(socket_fd); +} + +context::~context() +{ +} + +std::shared_ptr context::create(int socket_fd) +{ + // NOTE: can't use make_shared() because of the protected ctor */ + return std::shared_ptr(new context(socket_fd)); +} + +std::shared_ptr context::create_tcp(const std::string& domain, + const std::string& port, std::string *error) +{ + int fd = hwstub::net::socket_connection::create_tcp(domain, port, error); + if(fd >= 0) + return context::create(fd); + else + return std::shared_ptr(); +} + +error context::fetch_device_list(std::vector& list, void*& ptr) +{ + (void) ptr; + list.resize(1); + list[0] = 0; + return error::SUCCESS; +} + +void context::destroy_device_list(void *ptr) +{ + (void) ptr; +} + +error context::create_device(ctx_dev_t dev, std::shared_ptr& hwdev) +{ + /* there is only one device, so we don't use dev */ + (void) dev; + // NOTE: can't use make_shared() because of the protected ctor */ + hwdev.reset(new device(shared_from_this())); + return error::SUCCESS; +} + +bool context::match_device(ctx_dev_t dev, std::shared_ptr hwdev) +{ + (void) dev; + (void) hwdev; + /* there is only one device, so they all match */ + return true; +} + +/** + * Device + */ +device::device(std::shared_ptr ctx) + :hwstub::device(ctx) +{ +} + +device::~device() +{ +} + +error device::open_dev(std::shared_ptr& h) +{ + // NOTE: can't use make_shared() because of the protected ctor */ + h.reset(new handle(shared_from_this())); + return error::SUCCESS; +} + +bool device::has_multiple_open() const +{ + /* we want to serialize accesses, otherwise multiple concurrent messages to + * the server over the same connection will confuse the server */ + return false; +} + +/** + * Handle + */ +handle::handle(std::shared_ptr dev) + :hwstub::handle(dev) +{ + m_desc_version.bLength = sizeof(m_desc_version); + m_desc_version.bDescriptorType = HWSTUB_DT_VERSION; + m_desc_version.bMajor = HWSTUB_VERSION_MAJOR; + m_desc_version.bMinor = HWSTUB_VERSION_MINOR; + m_desc_version.bRevision = 0; + + m_desc_layout.bLength = sizeof(m_desc_layout); + m_desc_layout.bDescriptorType = HWSTUB_DT_LAYOUT; + m_desc_layout.dCodeStart = 0; + m_desc_layout.dCodeSize = 0; + m_desc_layout.dStackStart = 0; + m_desc_layout.dStackSize = 0; + m_desc_layout.dBufferStart = 0; + m_desc_layout.dBufferSize = 1; + + m_desc_target.bLength = sizeof(m_desc_target); + m_desc_target.bDescriptorType = HWSTUB_DT_TARGET; + m_desc_target.dID = HWSTUB_TARGET_UNK; + strncpy(m_desc_target.bName, "GDB server", sizeof(m_desc_target.bName)); + m_desc_target.bName[sizeof(m_desc_target.bName) - 1] = 0; +} + +handle::~handle() +{ +} + + +error handle::read_dev(uint32_t addr, void *buf, size_t& sz, bool atomic) +{ + std::shared_ptr hctx = get_device()->get_context(); + if(!hctx) + return error::NO_CONTEXT; + context *ctx = dynamic_cast(hctx.get()); + ctx->debug() << "[gdb::handle] --> READ(0x" << std::hex + << addr << "," << sz << "," << atomic << ")\n"; + ctx->debug() << "[gdb::handle] <-- READ unimplemented\n"; + /* implement read here using ctx->send() and ctx->recv() */ + (void) addr; + (void) buf; + (void) sz; + (void) atomic; + return error::DUMMY; +} + +error handle::write_dev(uint32_t addr, const void *buf, size_t& sz, bool atomic) +{ + std::shared_ptr hctx = get_device()->get_context(); + if(!hctx) + return error::NO_CONTEXT; + context *ctx = dynamic_cast(hctx.get()); + ctx->debug() << "[gdb::handle] --> WRITE(0x" << std::hex + << addr << "," << sz << "," << atomic << ")\n"; + ctx->debug() << "[gdb::handle] <-- WRITE unimplemented\n"; + /* implement read here using ctx.send() and ctx.recv() */ + (void) addr; + (void) buf; + (void) sz; + (void) atomic; + return error::DUMMY; +} + +error handle::get_dev_desc(uint16_t desc, void *buf, size_t& buf_sz) +{ + void *p = nullptr; + switch(desc) + { + case HWSTUB_DT_VERSION: p = &m_desc_version; break; + case HWSTUB_DT_LAYOUT: p = &m_desc_layout; break; + case HWSTUB_DT_TARGET: p = &m_desc_target; break; + default: break; + } + if(p == nullptr) + return error::ERROR; + /* size is in the bLength field of the descriptor */ + size_t desc_sz = *(uint8_t *)p; + buf_sz = std::min(buf_sz, desc_sz); + memcpy(buf, p, buf_sz); + return error::SUCCESS; +} + +error handle::get_dev_log(void *buf, size_t& buf_sz) +{ + (void) buf; + (void) buf_sz; + return error::DUMMY; +} + +error handle::exec_dev(uint32_t addr, uint16_t flags) +{ + (void) addr; + (void) flags; + return error::DUMMY; +} + +error handle::status() const +{ + return hwstub::handle::status(); +} + +size_t handle::get_buffer_size() +{ + return 2048; +} + +} // namespace gdb +} // namespace net + diff --git a/utils/hwstub/lib/hwstub_net.cpp b/utils/hwstub/lib/hwstub_net.cpp index 851723f37d..7009295bd8 100644 --- a/utils/hwstub/lib/hwstub_net.cpp +++ b/utils/hwstub/lib/hwstub_net.cpp @@ -29,39 +29,6 @@ namespace hwstub { namespace net { -/** - * Context - */ -context::context() - :m_state(state::HELLO), m_error(error::SUCCESS) -{ -} - -context::~context() -{ -} - -std::shared_ptr context::create_socket(int socket_fd) -{ - // NOTE: can't use make_shared() because of the protected ctor */ - return std::shared_ptr(new socket_context(socket_fd)); -} - -std::string context::default_unix_path() -{ - return "hwstub"; -} - -std::string context::default_tcp_domain() -{ - return "localhost"; -} - -std::string context::default_tcp_port() -{ - return "6666"; -} - namespace { /* len is the total length, including a 0 character if any */ @@ -171,10 +138,163 @@ namespace } } +/** + * Socket connection + */ +socket_connection::socket_connection() + :m_socketfd(-1) +{ +} + +socket_connection::~socket_connection() +{ + close(); +} + +int socket_connection::get_socket_fd() +{ + return m_socketfd; +} + +void socket_connection::init(int socket_fd) +{ + close(); + m_socketfd = socket_fd; +} + +void socket_connection::close() +{ + if(m_socketfd >= 0) + ::close(m_socketfd); + m_socketfd = -1; +} + +int socket_connection::create_tcp(const std::string& domain, + const std::string& port, std::string *error) +{ + return create_tcp_low(domain, port, false, error); +} + +int socket_connection::create_unix(const std::string& path, + std::string *error) +{ + return create_unix_low(false, path.c_str(), path.size() + 1, true, error); +} + +int socket_connection::create_unix_abstract(const std::string& path, + std::string *error) +{ + std::string fake_path = "#" + path; /* the # will be overriden by 0 */ + return create_unix_low(true, fake_path.c_str(), fake_path.size(), true, error); +} + +uint32_t socket_connection::to_net_order(uint32_t u) +{ + return htonl(u); +} + +uint32_t socket_connection::from_net_order(uint32_t u) +{ + return ntohl(u); +} + +void socket_connection::set_timeout(std::chrono::milliseconds ms) +{ + struct timeval tv; + tv.tv_usec = 1000 * (ms.count() % 1000); + tv.tv_sec = ms.count() / 1000; + /* set timeout for the client operations */ + setsockopt(m_socketfd, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv)); + setsockopt(m_socketfd, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv)); +} + +error socket_connection::send(void *buffer, size_t& sz, std::ostream& debug) +{ + debug << "[net::sock] send(" << sz << "): "; + int ret = ::send(m_socketfd, buffer, sz, MSG_NOSIGNAL); + if(ret >= 0) + { + debug << "good(" << ret << ")\n"; + sz = (size_t)ret; + return error::SUCCESS; + } + /* convert some errors */ + debug << "fail(" << errno << "," << strerror(errno) << ")\n"; + switch(errno) + { +#if EAGAIN != EWOULDBLOCK + case EAGAIN: +#endif + case EWOULDBLOCK: return error::TIMEOUT; + case ECONNRESET: case EPIPE: return error::SERVER_DISCONNECTED; + default: return error::NET_ERROR; + } +} + +error socket_connection::recv(void *buffer, size_t& sz, std::ostream& debug) +{ + debug << "[net::sock] recv(" << sz << "): "; + int ret = ::recv(m_socketfd, buffer, sz, MSG_WAITALL); + if(ret > 0) + { + debug << "good(" << ret << ")\n"; + sz = (size_t)ret; + return error::SUCCESS; + } + if(ret == 0) + { + debug << "disconnected\n"; + return error::SERVER_DISCONNECTED; + } + debug << "fail(" << errno << "," << strerror(errno) << ")\n"; + switch(errno) + { +#if EAGAIN != EWOULDBLOCK + case EAGAIN: +#endif + case EWOULDBLOCK: return error::TIMEOUT; + default: return error::NET_ERROR; + } +} + + +/** + * Context + */ +context::context() + :m_state(state::HELLO), m_error(error::SUCCESS) +{ +} + +context::~context() +{ +} + +std::shared_ptr context::create_socket(int socket_fd) +{ + // NOTE: can't use make_shared() because of the protected ctor */ + return std::shared_ptr(new socket_context(socket_fd)); +} + +std::string context::default_unix_path() +{ + return "hwstub"; +} + +std::string context::default_tcp_domain() +{ + return "localhost"; +} + +std::string context::default_tcp_port() +{ + return "6666"; +} + std::shared_ptr context::create_tcp(const std::string& domain, const std::string& port, std::string *error) { - int fd = create_tcp_low(domain, port, false, error); + int fd = socket_connection::create_tcp(domain, port, error); if(fd >= 0) return context::create_socket(fd); else @@ -183,7 +303,7 @@ std::shared_ptr context::create_tcp(const std::string& domain, std::shared_ptr context::create_unix(const std::string& path, std::string *error) { - int fd = create_unix_low(false, path.c_str(), path.size() + 1, true, error); + int fd = socket_connection::create_unix(path, error); if(fd >= 0) return context::create_socket(fd); else @@ -192,8 +312,7 @@ std::shared_ptr context::create_unix(const std::string& path, std::stri std::shared_ptr context::create_unix_abstract(const std::string& path, std::string *error) { - std::string fake_path = "#" + path; /* the # will be overriden by 0 */ - int fd = create_unix_low(true, fake_path.c_str(), fake_path.size(), true, error); + int fd = socket_connection::create_unix_abstract(path, error); if(fd >= 0) return context::create_socket(fd); else @@ -264,12 +383,12 @@ bool context::match_device(ctx_dev_t dev, std::shared_ptr hwdev) uint32_t context::to_net_order(uint32_t u) { - return htonl(u); + return socket_connection::to_net_order(u); } uint32_t context::from_net_order(uint32_t u) { - return ntohl(u); + return socket_connection::from_net_order(u); } error context::send_cmd(uint32_t cmd, uint32_t args[HWSTUB_NET_ARGS], uint8_t *send_data, @@ -492,74 +611,29 @@ void context::stop_context() * Socket context */ socket_context::socket_context(int socket_fd) - :m_socketfd(socket_fd) { + m_conn.init(socket_fd); set_timeout(std::chrono::milliseconds(1000)); } socket_context::~socket_context() { stop_context(); - close(m_socketfd); } void socket_context::set_timeout(std::chrono::milliseconds ms) { - struct timeval tv; - tv.tv_usec = 1000 * (ms.count() % 1000); - tv.tv_sec = ms.count() / 1000; - /* set timeout for the client operations */ - setsockopt(m_socketfd, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv)); - setsockopt(m_socketfd, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv)); + m_conn.set_timeout(ms); } error socket_context::send(void *buffer, size_t& sz) { - debug() << "[net::ctx::sock] send(" << sz << "): "; - int ret = ::send(m_socketfd, buffer, sz, MSG_NOSIGNAL); - if(ret >= 0) - { - debug() << "good(" << ret << ")\n"; - sz = (size_t)ret; - return error::SUCCESS; - } - /* convert some errors */ - debug() << "fail(" << errno << "," << strerror(errno) << ")\n"; - switch(errno) - { -#if EAGAIN != EWOULDBLOCK - case EAGAIN: -#endif - case EWOULDBLOCK: return error::TIMEOUT; - case ECONNRESET: case EPIPE: return error::SERVER_DISCONNECTED; - default: return error::NET_ERROR; - } + return m_conn.send(buffer, sz, debug()); } error socket_context::recv(void *buffer, size_t& sz) { - debug() << "[net::ctx::sock] recv(" << sz << "): "; - int ret = ::recv(m_socketfd, buffer, sz, MSG_WAITALL); - if(ret > 0) - { - debug() << "good(" << ret << ")\n"; - sz = (size_t)ret; - return error::SUCCESS; - } - if(ret == 0) - { - debug() << "disconnected\n"; - return error::SERVER_DISCONNECTED; - } - debug() << "fail(" << errno << "," << strerror(errno) << ")\n"; - switch(errno) - { -#if EAGAIN != EWOULDBLOCK - case EAGAIN: -#endif - case EWOULDBLOCK: return error::TIMEOUT; - default: return error::NET_ERROR; - } + return m_conn.recv(buffer, sz, debug()); } /** diff --git a/utils/hwstub/lib/hwstub_uri.cpp b/utils/hwstub/lib/hwstub_uri.cpp index da59671540..578f30b2ab 100644 --- a/utils/hwstub/lib/hwstub_uri.cpp +++ b/utils/hwstub/lib/hwstub_uri.cpp @@ -21,6 +21,7 @@ #include "hwstub_usb.hpp" #include "hwstub_virtual.hpp" #include "hwstub_net.hpp" +#include "hwstub_gdb.hpp" #include namespace hwstub { @@ -41,6 +42,8 @@ void print_usage(FILE *f, bool client, bool server) if(client) fprintf(f, " usb USB context (using libusb)\n"); fprintf(f, " tcp TCP context\n"); + if(client) + fprintf(f, " gdb A GDB context over tcp\n"); fprintf(f, " unix Local unix domain context\n"); if(client) fprintf(f, " virt A virtual context (testing and debugging mostly)\n"); @@ -63,6 +66,8 @@ void print_usage(FILE *f, bool client, bool server) fprintf(f, "in which case the domain must start with a '#':\n"); fprintf(f, " unix:///path/to/socket\n"); fprintf(f, " unix://#hwstub\n"); + fprintf(f, "When creating a GDB context, the domain and port are the usual TCP parameters:\n"); + fprintf(f, " gdb://localhost:4141\n"); if(client) { fprintf(f, "When creating a virtual context, the domain must contain a specification of the devices.\n"); @@ -288,6 +293,10 @@ std::shared_ptr create_context(const uri& uri, std::string *error) { return hwstub::net::context::create_tcp(uri.domain(), uri.port(), error); } + else if(uri.scheme() == "gdb") + { + return hwstub::gdb::context::create_tcp(uri.domain(), uri.port(), error); + } else if(uri.scheme() == "unix") { /* port must be empty */ From 73b1afabd38fdf390d4e1439109cbcdda664e5ef Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Sat, 3 Oct 2026 11:53:07 -0400 Subject: [PATCH 400/408] plugins: test_cyc, a per-instruction cycle benchmark A test plugin that measures what ARM instructions cost on the running device. Each row times sixteen copies of one instruction in a loop and subtracts the empty loop, giving cycles per instruction. Where plugins get IRAM, each row is run with code and data in each combination of IRAM and DRAM. It covers ALU, shifts, branches, the load and store forms (including halfword and register-offset loads), load-use, and the multiplier at narrow and full-width operands. ARMv5 adds the DSP multiplies, clz, qadd, ldrd/strd and pld; ARMv6 adds the top-word and dual 16-bit multiplies, umaal, ssat, rev, the extends, pkhbt, the SIMD adds and multiply result latency. Cache misses are priced by a pointer chase and by sequential streams over working sets either side of the cache. Timing uses the SoC's microsecond counter on PP502x, PP5002, S5L870x, S5L8720, TCC7801 and i.MX233, and the tick elsewhere. Results go to the screen and to /test_cyc.txt. Built only with test plugins enabled, on native ARM targets that run ARM code (not Cortex-M). Co-Authored-By: Claude Opus 5.5 Change-Id: I4a41f6daaa27fb817f411de89901a117c5c70f4d --- apps/plugins/CATEGORIES | 1 + apps/plugins/SOURCES | 4 + apps/plugins/test_cyc.c | 534 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 539 insertions(+) create mode 100644 apps/plugins/test_cyc.c diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES index bf646a5bf9..245be6cc15 100644 --- a/apps/plugins/CATEGORIES +++ b/apps/plugins/CATEGORIES @@ -179,6 +179,7 @@ sudoku,games test_boost,apps test_mem,apps test_codec,viewers +test_cyc,apps test_disk,apps test_fps,apps test_grey,apps diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES index 797601e4dd..226903f35f 100644 --- a/apps/plugins/SOURCES +++ b/apps/plugins/SOURCES @@ -217,6 +217,10 @@ test_codec.c #ifdef HAVE_JPEG test_core_jpeg.c #endif +#if (CONFIG_PLATFORM & PLATFORM_NATIVE) && defined(CPU_ARM) \ + && !defined(CPU_ARM_MICRO) +test_cyc.c +#endif test_disk.c test_fps.c test_gfx.c diff --git a/apps/plugins/test_cyc.c b/apps/plugins/test_cyc.c new file mode 100644 index 0000000000..2a34e7795d --- /dev/null +++ b/apps/plugins/test_cyc.c @@ -0,0 +1,534 @@ +/*************************************************************************** +* __________ __ ___. +* Open \______ \ ____ ____ | | _\_ |__ _______ ___ +* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / +* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < +* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ +* \/ \/ \/ \/ \/ +* $Id$ +* +* Copyright (C) 2026 Michael Giacomelli +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +* KIND, either express or implied. +* +****************************************************************************/ + +/* Measures what instructions cost on the running device, in CPU cycles. + * + * Each benchmark is sixteen copies of one instruction and the loop control. + * Where plugins get IRAM it is timed with the code in IRAM and in DRAM, and + * with the data it touches in IRAM and in DRAM (a 256-byte buffer, so the + * DRAM copy stays cached). The empty loop is timed the same way and + * subtracted, so every figure is cycles per instruction. Targets without + * plugin IRAM print one column. + * + * Stores are also timed with ALU instructions between them, whose cost is + * subtracted, to tell a cost of back-to-back stores from one of code and + * data sharing a memory. + * + * Cache misses are priced two ways over the plugin buffer. A pointer chase, + * where every load's address is the value the previous load returned, gives + * the full latency of a miss with nothing overlapping it. Streams of + * independent loads, a word, a 32-byte line and a four-word ldm at a time, + * give what a miss costs when it is free to overlap the loads around it. + * + * ARMv5 adds the DSP multiplies, clz, qadd, ldrd/strd and pld; ARMv6 the + * top-word and dual 16-bit multiplies, umaal, ssat, rev, the extends, pkhbt, + * the SIMD adds and multiply result latency. Each set is behind #if + * ARM_ARCH so the file builds for every ARM target. + * + * Runs are timed with the finest free-running timer the SoC has, or failing + * that with the tick, stretching each run to a hundred ticks, which keeps + * the figures to about a percent. + * + * Results go to the screen and to /test_cyc.txt. + */ + +#include "plugin.h" + +#define ITERS 200000ul /* first try at loop iterations per run */ +#define UNROLL 16 + +#define X16(i) i i i i i i i i i i i i i i i i + +/* The data a benchmark touches: small, so the DRAM copy is a cache hit. */ +static unsigned long iram_buf[64] IBSS_ATTR __attribute__((aligned(32))); +static unsigned long dram_buf[64] __attribute__((aligned(32))); + + +typedef void (*benchfn)(unsigned long *p, unsigned long n); + +/* One benchmark, twice: once in IRAM, once in DRAM. r4-r9 are free for the + body; p and n come in whatever registers the compiler picks. */ +#define BENCH(name, setup, body) \ +static void ICODE_ATTR UNUSED_ATTR \ +name##_i(unsigned long *p, unsigned long n) \ +{ \ + asm volatile (setup "1:\n" X16(body) \ + " subs %[n], %[n], #1\n" \ + " bne 1b\n" \ + : [n] "+r" (n) : [p] "r" (p) \ + : "r4", "r5", "r6", "r7", "r8", "r9", "cc", "memory"); \ +} \ +static void name##_d(unsigned long *p, unsigned long n) \ +{ \ + asm volatile (setup "1:\n" X16(body) \ + " subs %[n], %[n], #1\n" \ + " bne 1b\n" \ + : [n] "+r" (n) : [p] "r" (p) \ + : "r4", "r5", "r6", "r7", "r8", "r9", "cc", "memory"); \ +} + +#define NOSETUP "" +#define MULSETUP " mov r6, #0x3f00\n orr r6, r6, #0xff\n mov r7, r6\n" +/* A full-width multiplier, so the ARM7TDMI makes all four passes. */ +#define MUL32SETUP " mvn r6, #0x80000000\n mov r7, r6\n" +/* Offsets for the register-offset loads: 4 bytes, or 1 word scaled. */ +#define OFFSETUP " mov r6, #4\n mov r7, #1\n" + +BENCH(b_empty, NOSETUP, "") +BENCH(b_alu, NOSETUP, " add r4, r5, #1\n") +BENCH(b_regshift, NOSETUP, " mov r4, r5, lsl r6\n") +BENCH(b_branch, NOSETUP, " b .+4\n") +BENCH(b_ldr, NOSETUP, " ldr r4, [%[p]]\n" + " ldr r5, [%[p], #4]\n") +BENCH(b_ldruse, NOSETUP, " ldr r4, [%[p]]\n add r5, r4, #1\n") +BENCH(b_ldrb, NOSETUP, " ldrb r4, [%[p]]\n" + " ldrb r5, [%[p], #4]\n") +BENCH(b_ldm2, NOSETUP, " ldmia %[p], { r4, r5 }\n") +BENCH(b_ldm4, NOSETUP, " ldmia %[p], { r4-r7 }\n") +BENCH(b_ldm6, NOSETUP, " ldmia %[p], { r4-r9 }\n") +BENCH(b_str, NOSETUP, " str r4, [%[p]]\n") +BENCH(b_stm2, NOSETUP, " stmia %[p], { r4, r5 }\n") +BENCH(b_stm4, NOSETUP, " stmia %[p], { r4-r7 }\n") +BENCH(b_str1, NOSETUP, " str r4, [%[p]]\n add r5, r5, #1\n") +BENCH(b_str2, NOSETUP, " str r4, [%[p]]\n add r5, r5, #1\n" + " add r6, r6, #1\n") +BENCH(b_stm2gap, NOSETUP, " stmia %[p], { r4, r5 }\n" + " add r6, r6, #1\n" + " add r7, r7, #1\n") +BENCH(b_strldr, NOSETUP, " str r4, [%[p]]\n" + " ldr r5, [%[p], #4]\n") +BENCH(b_ldrbuse, NOSETUP, " ldrb r4, [%[p]]\n add r5, r4, #1\n") +BENCH(b_ldm2use, NOSETUP, " ldmia %[p], { r4, r5 }\n" + " add r6, r5, #1\n") +BENCH(b_ldrh, NOSETUP, " ldrh r4, [%[p]]\n" + " ldrh r5, [%[p], #4]\n") +BENCH(b_ldrsh, NOSETUP, " ldrsh r4, [%[p]]\n" + " ldrsh r5, [%[p], #4]\n") +BENCH(b_ldrhuse, NOSETUP, " ldrh r4, [%[p]]\n add r5, r4, #1\n") +BENCH(b_ldrshuse, NOSETUP, " ldrsh r4, [%[p]]\n add r5, r4, #1\n") +BENCH(b_ldrreg, OFFSETUP, " ldr r4, [%[p], r6]\n" + " ldr r5, [%[p], r6]\n") +BENCH(b_ldrlsl, OFFSETUP, " ldr r4, [%[p], r7, lsl #2]\n" + " ldr r5, [%[p], r7, lsl #2]\n") +BENCH(b_ldrbreg, OFFSETUP, " ldrb r4, [%[p], r6]\n" + " ldrb r5, [%[p], r6]\n") +BENCH(b_ldrhreg, OFFSETUP, " ldrh r4, [%[p], r6]\n" + " ldrh r5, [%[p], r6]\n") +BENCH(b_ldrreguse, OFFSETUP, " ldr r4, [%[p], r6]\n" + " add r5, r4, #1\n") +BENCH(b_mla, MULSETUP, " mla r4, r5, r6, r4\n") +BENCH(b_mla32, MUL32SETUP, " mla r4, r5, r6, r4\n") +BENCH(b_smlal32, MUL32SETUP, " smlal r4, r5, r8, r7\n") +#if ARM_ARCH >= 5 +BENCH(b_smulbb, MULSETUP, " smulbb r4, r5, r6\n") +BENCH(b_smlabb, MULSETUP, " smlabb r4, r5, r6, r4\n") +BENCH(b_smlalbb, MULSETUP, " smlalbb r4, r5, r8, r6\n") +BENCH(b_smulwb, MULSETUP, " smulwb r4, r5, r6\n") +BENCH(b_smlawb, MULSETUP, " smlawb r4, r5, r6, r4\n") +BENCH(b_clz, NOSETUP, " clz r4, r5\n") +BENCH(b_qadd, NOSETUP, " qadd r4, r5, r6\n") +BENCH(b_ldrd, NOSETUP, " ldrd r4, r5, [%[p]]\n") +BENCH(b_strd, NOSETUP, " strd r4, r5, [%[p]]\n") +BENCH(b_pld, NOSETUP, " pld [%[p]]\n") +#endif +#if ARM_ARCH >= 6 +BENCH(b_smmul, MULSETUP, " smmul r4, r5, r6\n") +BENCH(b_smmla, MULSETUP, " smmla r4, r5, r6, r4\n") +BENCH(b_smmulr, MULSETUP, " smmulr r4, r5, r6\n") +BENCH(b_smuad, MULSETUP, " smuad r4, r5, r6\n") +BENCH(b_smlad, MULSETUP, " smlad r4, r5, r6, r4\n") +BENCH(b_smlald, MULSETUP, " smlald r4, r5, r8, r6\n") +BENCH(b_umaal, MULSETUP, " umaal r4, r5, r8, r6\n") +BENCH(b_muluse, MULSETUP, " mul r4, r5, r6\n add r7, r4, #1\n") +BENCH(b_smulluse, MULSETUP, " smull r4, r5, r8, r6\n" + " add r7, r5, #1\n") +BENCH(b_ssat, NOSETUP, " ssat r4, #16, r5\n") +BENCH(b_rev, NOSETUP, " rev r4, r5\n") +BENCH(b_sxth, NOSETUP, " sxth r4, r5\n") +BENCH(b_pkhbt, NOSETUP, " pkhbt r4, r5, r6, lsl #16\n") +BENCH(b_sadd16, NOSETUP, " sadd16 r4, r5, r6\n") +BENCH(b_qadd16, NOSETUP, " qadd16 r4, r5, r6\n") +#endif +BENCH(b_smlal, MULSETUP, " smlal r4, r5, r8, r7\n") + +/* A pointer chase: each word holds the address of the next one, stride + bytes on, wrapping at the end. Sixteen dependent loads per iteration, so + the loop's two instructions are a small share and every load waits for the + last. */ +static void chase(unsigned long *start, unsigned long n) +{ + asm volatile ( + " mov r4, %[s]\n" + "1:\n" + X16(" ldr r4, [r4]\n") + " subs %[n], %[n], #1\n" + " bne 1b\n" + : [n] "+r" (n) : [s] "r" (start) : "r4", "cc", "memory"); +} + +/* Sequential loads through sbuf, which is sbytes long (a multiple of 512), + rewinding at the end. r4-r7 take the data and nothing reads it. p and + n are early-clobber: p starts equal to sbuf, and gcc would otherwise give + p and base one register, making the rewind a no-op. */ +static unsigned long *sbuf; +static unsigned long sbytes; + +#define STREAM(name, body) \ +static void name(unsigned long *unused, unsigned long n) \ +{ \ + unsigned long *p = sbuf, *end = sbuf + sbytes / 4; \ + (void)unused; \ + asm volatile ("1:\n" X16(body) \ + " cmp %[p], %[end]\n" \ + " movhs %[p], %[base]\n" \ + " subs %[n], %[n], #1\n" \ + " bne 1b\n" \ + : [n] "+&r" (n), [p] "+&r" (p) \ + : [end] "r" (end), [base] "r" (sbuf) \ + : "r4", "r5", "r6", "r7", "cc", "memory"); \ +} +STREAM(stream_word, " ldr r4, [%[p]], #4\n") +STREAM(stream_line, " ldr r4, [%[p]], #32\n") +STREAM(stream_ldm4, " ldmia %[p]!, { r4-r7 }\n") + +static void make_chain(unsigned char *buf, unsigned long bytes, + unsigned long stride) +{ + unsigned long off; + for (off = 0; off < bytes; off += stride) + *(unsigned long *)(buf + off) = + (unsigned long)(buf + (off + stride) % bytes); +} + +/* The finest free-running timer each SoC has. Plugins do not see the SoC + headers, so the registers are named here, as USEC_TIMER (or the + S5L8700's 5 us counter) is defined for each in firmware/. Anywhere + else the tick has to do. */ +#if CONFIG_CPU == PP5020 || CONFIG_CPU == PP5022 || CONFIG_CPU == PP5024 +#define NOW() (*(volatile unsigned long *)0x60005010) +#define PER_SEC 1000000ll +#define TIMER_NAME "PP502x usec timer" +#elif CONFIG_CPU == S5L8700 || CONFIG_CPU == S5L8701 +#define NOW() (*(volatile unsigned long *)0x3C700084) /* TICNTL */ +#define PER_SEC 200000ll +#define TIMER_NAME "S5L8700 5 usec timer" +#elif CONFIG_CPU == PP5002 +#define NOW() (*(volatile unsigned long *)0xcf001110) +#define PER_SEC 1000000ll +#define TIMER_NAME "PP5002 usec timer" +#elif CONFIG_CPU == S5L8702 || CONFIG_CPU == S5L8720 +#define NOW() (*(volatile unsigned long *)0x3C7000B4) /* TECNT */ +#define PER_SEC 1000000ll +#define TIMER_NAME "S5L8702/8720 usec timer" +#elif CONFIG_CPU == TCC7801 +#define NOW() (*(volatile unsigned long *)0xF3003094) /* TC32MCNT */ +#define PER_SEC 1000000ll +#define TIMER_NAME "TCC7801 usec timer" +#elif CONFIG_CPU == IMX233 +#define NOW() (*(volatile unsigned long *)0x8001c0c0) /* DIGCTL usec */ +#define PER_SEC 1000000ll +#define TIMER_NAME "i.MX233 usec timer" +#else +#define NOW() (*rb->current_tick) +#define PER_SEC ((long long)HZ) +#define TICK_TIMED +#define TIMER_NAME "the tick: runs of a second or more" +#endif + +#ifdef TICK_TIMED +#define MIN_ELAPSED 100ul /* a second of ticks */ +#else +#define MIN_ELAPSED ((unsigned long)(PER_SEC / 50)) /* 20 ms */ +#endif + +/* Hundredths of a cycle per loop iteration. The run doubles in length until + the clock has moved far enough for the figure to be good to a percent. */ +static long long cpi(benchfn fn, unsigned long *p) +{ + unsigned long n = ITERS, t0, t1; + fn(p, 64); /* warm the caches */ + for (;;) + { + t0 = NOW(); + fn(p, n); + t1 = NOW(); + if (t1 - t0 >= MIN_ELAPSED || n >= (1ul << 28)) + break; + n *= 2; + } + return (long long)(t1 - t0) * *rb->cpu_frequency * 100 / PER_SEC / n; +} + +static int fd = -1; + +/* The screen keeps as many of the latest lines as fit, older ones scrolling + off the top; the file gets every line. */ +#define MAXROWS 64 +static char shown[MAXROWS][48]; +static int nshown; + +static void say(const char *fmt, ...) +{ + char buf[96]; + int i, rows; + va_list ap; + va_start(ap, fmt); + rb->vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + if (fd >= 0) + { + rb->write(fd, buf, rb->strlen(buf)); + rb->write(fd, "\n", 1); + } + rows = LCD_HEIGHT / rb->font_get(FONT_SYSFIXED)->height; + if (rows > MAXROWS) + rows = MAXROWS; + if (rows < 1) + rows = 1; + if (nshown >= rows) + { + rb->memmove(shown[0], shown[nshown - rows + 1], + (rows - 1) * sizeof(shown[0])); + nshown = rows - 1; + } + rb->strlcpy(shown[nshown++], buf, sizeof(shown[0])); + rb->lcd_clear_display(); + for (i = 0; i < nshown; i++) + rb->lcd_puts(0, i, shown[i]); + rb->lcd_update(); +} + +/* Hundredths of a cycle per instruction, over the loop's own cost. */ +static int per_insn(long long cyc, long long empty) +{ + return (int)((cyc - empty) / UNROLL); +} + +static const struct { + const char *name; + benchfn i, d; + int per; /* instructions per X16 copy */ + int alu; /* ALU cycles per copy, subtracted */ +} B[] = { + { "add ", b_alu_i, b_alu_d, 1, 0 }, + { "mov lsl Rs", b_regshift_i, b_regshift_d, 1, 0 }, + { "b taken ", b_branch_i, b_branch_d, 1, 0 }, + { "ldr ", b_ldr_i, b_ldr_d, 2, 0 }, + { "ldr+use ", b_ldruse_i, b_ldruse_d, 1, 0 }, + { "ldrb ", b_ldrb_i, b_ldrb_d, 2, 0 }, + { "ldm 2 ", b_ldm2_i, b_ldm2_d, 1, 0 }, + { "ldm 4 ", b_ldm4_i, b_ldm4_d, 1, 0 }, + { "ldm 6 ", b_ldm6_i, b_ldm6_d, 1, 0 }, + { "str ", b_str_i, b_str_d, 1, 0 }, + { "stm 2 ", b_stm2_i, b_stm2_d, 1, 0 }, + { "stm 4 ", b_stm4_i, b_stm4_d, 1, 0 }, + { "str,1 alu ", b_str1_i, b_str1_d, 1, 1 }, + { "str,2 alu ", b_str2_i, b_str2_d, 1, 2 }, + { "stm2,2 alu", b_stm2gap_i, b_stm2gap_d, 1, 2 }, + { "str+ldr ", b_strldr_i, b_strldr_d, 1, 0 }, + { "ldrb+use ", b_ldrbuse_i, b_ldrbuse_d, 1, 1 }, + { "ldm2+use ", b_ldm2use_i, b_ldm2use_d, 1, 1 }, + { "ldrh ", b_ldrh_i, b_ldrh_d, 2, 0 }, + { "ldrsh ", b_ldrsh_i, b_ldrsh_d, 2, 0 }, + { "ldrh+use ", b_ldrhuse_i, b_ldrhuse_d, 1, 1 }, + { "ldrsh+use ", b_ldrshuse_i, b_ldrshuse_d, 1, 1 }, + { "ldr [Rn,Rm", b_ldrreg_i, b_ldrreg_d, 2, 0 }, + { "ldr Rm lsl", b_ldrlsl_i, b_ldrlsl_d, 2, 0 }, + { "ldrb Rm ", b_ldrbreg_i, b_ldrbreg_d, 2, 0 }, + { "ldrh Rm ", b_ldrhreg_i, b_ldrhreg_d, 2, 0 }, + { "ldr Rm+use", b_ldrreguse_i, b_ldrreguse_d, 1, 1 }, + { "mla 14b Rs", b_mla_i, b_mla_d, 1, 0 }, + { "smlal 14b ", b_smlal_i, b_smlal_d, 1, 0 }, + { "mla 32b Rs", b_mla32_i, b_mla32_d, 1, 0 }, + { "smlal 32b ", b_smlal32_i, b_smlal32_d, 1, 0 }, +#if ARM_ARCH >= 5 + { "smulbb ", b_smulbb_i, b_smulbb_d, 1, 0 }, + { "smlabb acc", b_smlabb_i, b_smlabb_d, 1, 0 }, + { "smlalbb ", b_smlalbb_i, b_smlalbb_d, 1, 0 }, + { "smulwb ", b_smulwb_i, b_smulwb_d, 1, 0 }, + { "smlawb acc", b_smlawb_i, b_smlawb_d, 1, 0 }, + { "clz ", b_clz_i, b_clz_d, 1, 0 }, + { "qadd ", b_qadd_i, b_qadd_d, 1, 0 }, + { "ldrd ", b_ldrd_i, b_ldrd_d, 1, 0 }, + { "strd ", b_strd_i, b_strd_d, 1, 0 }, + { "pld ", b_pld_i, b_pld_d, 1, 0 }, +#endif +#if ARM_ARCH >= 6 + { "smmul ", b_smmul_i, b_smmul_d, 1, 0 }, + { "smmla acc ", b_smmla_i, b_smmla_d, 1, 0 }, + { "smmulr ", b_smmulr_i, b_smmulr_d, 1, 0 }, + { "smuad ", b_smuad_i, b_smuad_d, 1, 0 }, + { "smlad acc ", b_smlad_i, b_smlad_d, 1, 0 }, + { "smlald ", b_smlald_i, b_smlald_d, 1, 0 }, + { "umaal ", b_umaal_i, b_umaal_d, 1, 0 }, + { "mul+use ", b_muluse_i, b_muluse_d, 1, 1 }, + { "smull+use ", b_smulluse_i, b_smulluse_d, 1, 1 }, + { "ssat ", b_ssat_i, b_ssat_d, 1, 0 }, + { "rev ", b_rev_i, b_rev_d, 1, 0 }, + { "sxth ", b_sxth_i, b_sxth_d, 1, 0 }, + { "pkhbt ", b_pkhbt_i, b_pkhbt_d, 1, 0 }, + { "sadd16 ", b_sadd16_i, b_sadd16_d, 1, 0 }, + { "qadd16 ", b_qadd16_i, b_qadd16_d, 1, 0 }, +#endif +}; +#define NB ((int)(sizeof(B) / sizeof(B[0]))) + +static void fmt(char *out, int v) +{ + rb->snprintf(out, 8, "%3d.%02d", v / 100, v < 0 ? -v % 100 : v % 100); +} + +enum plugin_status plugin_start(const void* parameter) +{ + long long e_d_d, loop_cyc; +#ifdef USE_IRAM + long long e_i_i, e_i_d, e_d_i; +#endif + int k; + (void)parameter; + + rb->lcd_setfont(FONT_SYSFIXED); + rb->lcd_clear_display(); + fd = rb->open("/test_cyc.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666); + if (fd < 0) + say("cannot create /test_cyc.txt (%d): screen only", fd); + +#ifdef HAVE_ADJUSTABLE_CPU_FREQ + rb->cpu_boost(true); +#endif + + /* subs and a taken branch: four cycles an iteration by the datasheet. + A different figure here scales every other number the same way. */ + e_d_d = cpi(b_empty_d, dram_buf); +#ifdef USE_IRAM + e_i_i = cpi(b_empty_i, iram_buf); + e_i_d = cpi(b_empty_i, dram_buf); + e_d_i = cpi(b_empty_d, iram_buf); + loop_cyc = e_i_i; +#else + loop_cyc = e_d_d; +#endif + say("ARMv%d, clock %ld MHz, loop %d.%02d cyc (want 4)", ARM_ARCH, + *rb->cpu_frequency / 1000000, (int)(loop_cyc / 100), + (int)(loop_cyc % 100)); + say("timer: %s", TIMER_NAME); +#ifdef USE_IRAM + say("cyc/insn code:IRAM IRAM DRAM DRAM"); + say(" data:IRAM DRAM IRAM DRAM"); +#else + say("no plugin IRAM: code and data in DRAM"); + say("cyc/insn"); +#endif + + for (k = 0; k < NB; k++) + { + char d[8]; + int div = B[k].per, sub = B[k].alu * 100; + fmt(d, (per_insn(cpi(B[k].d, dram_buf), e_d_d) - sub) / div); +#ifdef USE_IRAM + { + char a[8], b[8], c[8]; + fmt(a, (per_insn(cpi(B[k].i, iram_buf), e_i_i) - sub) / div); + fmt(b, (per_insn(cpi(B[k].i, dram_buf), e_i_d) - sub) / div); + fmt(c, (per_insn(cpi(B[k].d, iram_buf), e_d_i) - sub) / div); + say("%s %s %s %s %s", B[k].name, a, b, c, d); + } +#else + say("%s %s", B[k].name, d); +#endif + } + + /* Misses: a pointer chase over working sets either side of the cache, + in DRAM code, from the plugin buffer. The loop's own cost is the + empty loop's, which the DRAM-code, DRAM-data run measured above. */ + { + static const unsigned long sizes[] = { 1024, 4096, 8192, 16384, + 32768, 65536, 262144 }; + size_t bufsize; + unsigned char *big = rb->plugin_get_buffer(&bufsize); + unsigned long s32, k2; + big = (unsigned char *)(((unsigned long)big + 31) & ~31ul); + bufsize -= 32; + say("addr: iram %08lx dram %08lx chase %08lx", + (unsigned long)iram_buf, (unsigned long)dram_buf, + (unsigned long)big); + say("chase cyc/load stride 16 stride 32"); + for (k2 = 0; k2 < sizeof(sizes) / sizeof(sizes[0]); k2++) + { + char a[8], b[8]; + int v[2]; + unsigned long bytes = sizes[k2]; + if (bytes > bufsize) + break; + for (s32 = 0; s32 < 2; s32++) + { + make_chain(big, bytes, s32 ? 32 : 16); + v[s32] = per_insn(cpi(chase, (unsigned long *)big), e_d_d); + } + fmt(a, v[0]); + fmt(b, v[1]); + say(" %4lu KB %s %s", bytes / 1024, a, b); + } + + /* Streaming: the same plugin buffer, as large as it allows up to + 256 KB, against 1 KB of it. */ + { + static const struct { const char *name; benchfn fn; } S[] = { + { "word ", stream_word }, + { "line 32", stream_line }, + { "ldm 4 ", stream_ldm4 }, + }; + unsigned long large = bufsize > 262144 ? 262144 : bufsize; + large &= ~511ul; + say("stream cyc/load cached %luKB extra", large / 1024); + for (k2 = 0; k2 < sizeof(S) / sizeof(S[0]); k2++) + { + char a[8], b[8], c[8]; + int hit, miss; + sbuf = (unsigned long *)big; + sbytes = 1024; + hit = per_insn(cpi(S[k2].fn, NULL), e_d_d); + sbytes = large; + miss = per_insn(cpi(S[k2].fn, NULL), e_d_d); + fmt(a, hit); + fmt(b, miss); + fmt(c, miss - hit); + say(" %s %s %s %s", S[k2].name, a, b, c); + } + } + } + +#ifdef HAVE_ADJUSTABLE_CPU_FREQ + rb->cpu_boost(false); +#endif + if (fd >= 0) + { + rb->close(fd); + fd = -1; + say("wrote /test_cyc.txt, at the root of the main drive"); + } + + say("any key to exit"); + rb->button_clear_queue(); + rb->button_get(true); + rb->lcd_setfont(FONT_UI); + return PLUGIN_OK; +} From 49b4d8f2c9906d573da3b6218f5247fe5f9874b9 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Sun, 4 Oct 2026 19:21:50 -0400 Subject: [PATCH 401/408] test_cyc: AS3525v2 cache and write testing The rows that calibrated the Clip+ model in utils/perfsim, timed on AS3525v2 with the tick timer's count for a 2/3 microsecond clock: - calls, returns and loads into pc, interlocks after a load and after a multiply, multiplies by operand size - a miss with work after it, on each word of its line, back to back, and evicting a dirty line; a load or store to the line still filling - stores to lines not cached: one stream, a word a line, four words at a time, two streams turn about - copies between uncached buffers, within a memory and across - fetch misses in code written where it runs - loops of adds and of branches by how many lines they cover, and a cached pointer chase likewise - the TTA filter stage by stage and Tremor's window loop On AS3525v2 the miss rows run in DRAM and again in the RAM inside the SoC, where codecs are loaded. Rows that turned out to say nothing, or that a later row replaced, stay in the file under TEST_CYC_ARCHIVE, each with why. TEST_CYC_QUICK runs only the newest rows. With these rows the plugin is about 140 KB on ARMv5, so it is left out where the plugin buffer is 128 KB or less: the Clip, the m200v4 and the c200v2. Co-Authored-By: Claude Opus 5.5 Change-Id: I4ec45da514d0abaa02e1e041fd1a0387490994ed --- apps/plugins/SOURCES | 3 +- apps/plugins/test_cyc.c | 1817 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 1793 insertions(+), 27 deletions(-) diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES index 226903f35f..8e11baa23b 100644 --- a/apps/plugins/SOURCES +++ b/apps/plugins/SOURCES @@ -218,7 +218,8 @@ test_codec.c test_core_jpeg.c #endif #if (CONFIG_PLATFORM & PLATFORM_NATIVE) && defined(CPU_ARM) \ - && !defined(CPU_ARM_MICRO) + && !defined(CPU_ARM_MICRO) \ + && PLUGIN_BUFFER_SIZE > 0x20000 /* about 140 KB on ARMv5 */ test_cyc.c #endif test_disk.c diff --git a/apps/plugins/test_cyc.c b/apps/plugins/test_cyc.c index 2a34e7795d..6264b8b3a8 100644 --- a/apps/plugins/test_cyc.c +++ b/apps/plugins/test_cyc.c @@ -38,6 +38,20 @@ * independent loads, a word, a 32-byte line and a four-word ldm at a time, * give what a miss costs when it is free to overlap the loads around it. * + * Instructions whose condition fails are timed too (the loop leaves the + * carry set, so "cc" never executes), and so are stores to the stack, on + * this thread and again on the codec thread, whose stack a codec uses and + * which may not be in the memory the main thread's is. + * + * Where plugins have the room, a store from IRAM code is also timed at + * addresses 4 KB apart across IRAM, since that extra cycle depends on + * which part of IRAM the data is in. + * + * Three more cache costs are timed: a store to a line that is not cached, + * loads that miss far apart rather than next to each other, and an + * instruction fetch miss, from a chain of branches through more DRAM code + * than the cache holds. + * * ARMv5 adds the DSP multiplies, clz, qadd, ldrd/strd and pld; ARMv6 the * top-word and dual 16-bit multiplies, umaal, ssat, rev, the extends, pkhbt, * the SIMD adds and multiply result latency. Each set is behind #if @@ -52,6 +66,33 @@ #include "plugin.h" +/* Rows that have been run and told nothing the rows left in do not, or + that were a step to a finding since made another way. They are kept, + each group with a line on what it showed, and 1 here runs them again: + + - Instruction rows that came out as the sum of their parts on an + ARM926: a return through lr and bx, a conditional return, conditional + instructions that execute, flag-setting, a register shift in an add, + ldm of 3, the third register of an ldm used next, an ldm feeding a + multiply, writeback, stm then ldm, push and pop of ten, mul then use + (as mla then use), a load feeding a register shift. + - Multiplies with 24-bit and negative Rm, and mul, smull and smlal by + operand size: the same whatever the operands, as the rows kept show. + - A miss and then the rest of its line, back to back, which could not + tell a word arriving from the whole line arriving; "miss, then" can. + - Dirty evictions and a last-word miss with 16 instructions after: + the longer gaps kept say the same. Misses 64 and 256 instructions + apart, and an ldm of 2 after a miss: as their neighbours. + - The pair rewrite, a replay of FLAC's decorrelation in DRAM. It never + behaved like the codec, because on the device the codec is not in + DRAM. + - The filter by where its state and code sit, and frames of adds: + they led to "adds in a loop", which measures the cause directly. + - Lines kept after a stream of misses, and after new lines in a set: + too coarse and too noisy to tell how the victim is chosen; "first + line kept" does. */ +#define TEST_CYC_ARCHIVE 0 + #define ITERS 200000ul /* first try at loop iterations per run */ #define UNROLL 16 @@ -61,6 +102,14 @@ static unsigned long iram_buf[64] IBSS_ATTR __attribute__((aligned(32))); static unsigned long dram_buf[64] __attribute__((aligned(32))); +/* On PP5022/PP5024 plugins have 80 KB of IRAM, enough to map what a store + costs across most of it: the extra cycle a store pays when its code and + data share a memory turns out to depend on where in IRAM the data is. */ +#if (CONFIG_CPU == PP5022 || CONFIG_CPU == PP5024) && defined(USE_IRAM) +#define IRAM_MAP_BYTES (64 * 1024) +static unsigned long iram_map[IRAM_MAP_BYTES / 4] IBSS_ATTR; +#endif + typedef void (*benchfn)(unsigned long *p, unsigned long n); @@ -76,13 +125,19 @@ name##_i(unsigned long *p, unsigned long n) \ : [n] "+r" (n) : [p] "r" (p) \ : "r4", "r5", "r6", "r7", "r8", "r9", "cc", "memory"); \ } \ +BENCH_D(name, setup, body) + +/* The DRAM copy alone, for bodies that call the helpers below, which a + branch from IRAM would not reach. lr is free for the body too. */ +#define BENCH_D(name, setup, body) \ static void name##_d(unsigned long *p, unsigned long n) \ { \ asm volatile (setup "1:\n" X16(body) \ " subs %[n], %[n], #1\n" \ " bne 1b\n" \ : [n] "+r" (n) : [p] "r" (p) \ - : "r4", "r5", "r6", "r7", "r8", "r9", "cc", "memory"); \ + : "r4", "r5", "r6", "r7", "r8", "r9", "lr", "cc", \ + "memory"); \ } #define NOSETUP "" @@ -134,6 +189,21 @@ BENCH(b_ldrhreg, OFFSETUP, " ldrh r4, [%[p], r6]\n" " ldrh r5, [%[p], r6]\n") BENCH(b_ldrreguse, OFFSETUP, " ldr r4, [%[p], r6]\n" " add r5, r4, #1\n") +/* The loop's subs leaves the carry set while n has not run out, and this + sets it for the first pass, so a "cc" instruction never executes. */ +#define CCSETUP " cmp r4, r4\n" +BENCH(b_addcc, CCSETUP, " addcc r4, r5, #1\n") +BENCH(b_ldrcc, CCSETUP, " ldrcc r4, [%[p]]\n") +BENCH(b_strcc, CCSETUP, " strcc r4, [%[p]]\n") +BENCH(b_ldmcc, CCSETUP, " ldmccia %[p], { r4-r7 }\n") +BENCH(b_mlacc, MULSETUP CCSETUP, " mlacc r4, r5, r6, r4\n") +BENCH(b_bcc, CCSETUP, " bcc .+4\n") +/* Stack traffic, just below sp so nothing live is touched. */ +BENCH(b_spstr, NOSETUP, " str r4, [sp, #-4]\n") +BENCH(b_spstm, NOSETUP, " stmdb sp, { r4-r7 }\n") +BENCH(b_spldr, NOSETUP, " ldr r4, [sp, #-4]\n") +BENCH(b_sppush, NOSETUP, " stmfd sp!, { r4-r7 }\n" + " ldmfd sp!, { r4-r7 }\n") BENCH(b_mla, MULSETUP, " mla r4, r5, r6, r4\n") BENCH(b_mla32, MUL32SETUP, " mla r4, r5, r6, r4\n") BENCH(b_smlal32, MUL32SETUP, " smlal r4, r5, r8, r7\n") @@ -169,6 +239,104 @@ BENCH(b_qadd16, NOSETUP, " qadd16 r4, r5, r6\n") #endif BENCH(b_smlal, MULSETUP, " smlal r4, r5, r8, r7\n") +/* Calls and returns, results used by the next instruction, and conditional + instructions that execute: what compiled code does all the time and the + rows above do not. The helpers are called from DRAM code only. */ +void tc_leaf(void); +void tc_pop2(void); +void tc_poplr2(void); +void tc_pop10(void); +void tc_popne10(void); +__asm__(".text\n.align 2\n" + ".type tc_leaf, %function\n" + "tc_leaf:\n" + " bx lr\n" + ".type tc_pop2, %function\n" + "tc_pop2:\n" + " stmfd sp!, { r4, lr }\n" + " ldmfd sp!, { r4, pc }\n" + ".type tc_poplr2, %function\n" + "tc_poplr2:\n" + " stmfd sp!, { r4, lr }\n" + " ldmfd sp!, { r4, lr }\n" + " bx lr\n" + ".type tc_pop10, %function\n" + "tc_pop10:\n" + " stmfd sp!, { r4-r11, ip, lr }\n" + " ldmfd sp!, { r4-r11, ip, pc }\n" + ".type tc_popne10, %function\n" + "tc_popne10:\n" + " stmfd sp!, { r4-r11, ip, lr }\n" + " cmp sp, #0\n" + " ldmnefd sp!, { r4-r11, ip, pc }\n"); +/* The loop's subs leaves Z clear while n has not run out, and this clears + it for the first pass, so an "ne" instruction always executes. */ +#define NESETUP " cmp sp, #0\n" +BENCH_D(b_call, NOSETUP, " bl tc_leaf\n") +BENCH_D(b_callpop, NOSETUP, " bl tc_pop2\n") +#if TEST_CYC_ARCHIVE +BENCH_D(b_callpoplr, NOSETUP, " bl tc_poplr2\n") +#endif +BENCH_D(b_callpop10, NOSETUP, " bl tc_pop10\n") +#if TEST_CYC_ARCHIVE +BENCH_D(b_callpopne, NOSETUP, " bl tc_popne10\n") +#endif +BENCH_D(b_bx, NOSETUP, " adr r4, 2f\n bx r4\n2:\n") +BENCH_D(b_ldrpc, NOSETUP, " adr r4, 2f\n str r4, [%[p]]\n" + " ldr pc, [%[p]]\n2:\n") +BENCH_D(b_bne, NESETUP, " bne .+4\n") +#if TEST_CYC_ARCHIVE +BENCH_D(b_addne, NESETUP, " addne r4, r5, #1\n") +BENCH_D(b_ldmne, NESETUP, " ldmneia %[p], { r4-r7 }\n") +BENCH_D(b_stmne, NESETUP, " stmneia %[p], { r4-r7 }\n") +BENCH_D(b_ands, NOSETUP, " ands r4, r5, #63\n") +#endif +BENCH_D(b_orrasr, NOSETUP, " orr r4, r5, r6, asr #30\n") +#if TEST_CYC_ARCHIVE +BENCH_D(b_addasr, NOSETUP, " add r4, r5, r6, asr r7\n") +BENCH_D(b_ldm3, NOSETUP, " ldmia %[p], { r4, r5, lr }\n") +#endif +BENCH_D(b_ldm4first, NOSETUP, " ldmia %[p], { r4-r7 }\n" + " add r8, r4, #1\n") +#if TEST_CYC_ARCHIVE +BENCH_D(b_ldm4third, NOSETUP, " ldmia %[p], { r4-r7 }\n" + " add r8, r6, #1\n") +#endif +BENCH_D(b_ldm4last, NOSETUP, " ldmia %[p], { r4-r7 }\n" + " add r8, r7, #1\n") +#if TEST_CYC_ARCHIVE +BENCH_D(b_ldm4mla, NOSETUP, " ldmia %[p], { r4-r7 }\n" + " mla r8, r4, r5, r8\n") +BENCH_D(b_ldmwb, " mov r8, %[p]\n", + " ldmia r8!, { r4-r7 }\n sub r8, r8, #16\n") +BENCH_D(b_stmwb, " mov r8, %[p]\n", + " stmia r8!, { r4-r7 }\n sub r8, r8, #16\n") +BENCH_D(b_stmldm, " add r8, %[p], #32\n", + " stmia %[p], { r4-r7 }\n ldmia r8, { r4-r7 }\n") +#endif +BENCH_D(b_strldrsame, NOSETUP, " str r4, [%[p]]\n" + " ldr r5, [%[p]]\n") +#if TEST_CYC_ARCHIVE +BENCH_D(b_push10, NOSETUP, " stmfd sp!, { r4-r11, ip, lr }\n" + " ldmfd sp!, { r4-r11, ip, lr }\n") +#endif +BENCH_D(b_mlause, MULSETUP, " mla r4, r5, r6, r7\n" + " add r8, r4, #1\n") +BENCH_D(b_mlamul, MULSETUP, " mla r4, r5, r6, r7\n" + " mla r8, r4, r6, r7\n") +#if TEST_CYC_ARCHIVE +BENCH_D(b_muladd, MULSETUP, " mul r4, r5, r6\n" + " add r8, r4, #1\n") +#endif +BENCH_D(b_smullacc, MULSETUP, " smull r4, r5, r8, r6\n" + " add r7, r5, #1\n") +BENCH_D(b_ldrmla, MULSETUP, " ldr r4, [%[p]]\n" + " mla r8, r4, r6, r8\n") +#if TEST_CYC_ARCHIVE +BENCH_D(b_ldrshift, NOSETUP, " ldr r6, [%[p]]\n" + " add r4, r5, r4, asr r6\n") +#endif + /* A pointer chase: each word holds the address of the next one, stride bytes on, wrapping at the end. Sixteen dependent loads per iteration, so the loop's two instructions are a small share and every load waits for the @@ -208,6 +376,263 @@ static void name(unsigned long *unused, unsigned long n) \ STREAM(stream_word, " ldr r4, [%[p]], #4\n") STREAM(stream_line, " ldr r4, [%[p]], #32\n") STREAM(stream_ldm4, " ldmia %[p]!, { r4-r7 }\n") +/* The same with stores, to price a store whose line is not cached. */ +STREAM(wstream_word, " str r4, [%[p]], #4\n") +STREAM(wstream_line, " str r4, [%[p]], #32\n") +STREAM(wstream_stm4, " stmia %[p]!, { r4-r7 }\n") + +/* A load per line with work between that touches no memory, to see whether + a miss costs less when the next access is not waiting on it. */ +#define X8(i) i i i i i i i i +#define WORK8 X8(" add r5, r5, #1\n") +STREAM(stream_work8, " ldr r4, [%[p]], #32\n" WORK8) +STREAM(stream_work16, " ldr r4, [%[p]], #32\n" WORK8 WORK8) +STREAM(stream_work32, " ldr r4, [%[p]], #32\n" WORK8 WORK8 WORK8 WORK8) + +/* A miss on the last word of each line, and on its fourth: if the fill + brings the word asked for first these cost what a miss on the first word + does, and if it runs in order from the start of the line they cost + more. */ +STREAM(stream_last, " ldr r4, [%[p], #28]\n add %[p], %[p], #32\n") +STREAM(stream_mid, " ldr r4, [%[p], #12]\n add %[p], %[p], #32\n") +STREAM(stream_first, " ldr r4, [%[p]]\n add %[p], %[p], #32\n") +#if TEST_CYC_ARCHIVE +/* The same misses with the rest of the line read straight after, a word + at a time, in order from the start. */ +#define REST_OF_LINE \ + " ldr r5, [%[p]]\n ldr r5, [%[p], #4]\n" \ + " ldr r5, [%[p], #8]\n ldr r5, [%[p], #16]\n" \ + " ldr r5, [%[p], #20]\n ldr r5, [%[p], #24]\n" \ + " add %[p], %[p], #32\n" +STREAM(stream_last_rest, " ldr r4, [%[p], #28]\n" REST_OF_LINE) +STREAM(stream_mid_rest, " ldr r4, [%[p], #12]\n" REST_OF_LINE) +#endif + +/* A miss whose line is then written, so that every line a later miss + evicts is dirty and has to go back to memory first: one store dirties + half the line on the ARM926, two the whole of it. */ +STREAM(stream_dirty4, " ldr r4, [%[p]]\n str r4, [%[p]]\n" + " add %[p], %[p], #32\n") +STREAM(stream_dirty8, " ldr r4, [%[p]]\n str r4, [%[p]]\n" + " str r4, [%[p], #16]\n" + " add %[p], %[p], #32\n") + +/* The dirty eviction and the miss on a last word again, with work after + that touches no memory: whether the write-back, and the fill from the + last word, hold the core or only the next miss. */ +#define DIRTY8 " ldr r4, [%[p]]\n str r4, [%[p]]\n" \ + " str r4, [%[p], #16]\n add %[p], %[p], #32\n" +#define WORK16 WORK8 WORK8 +#define WORK32 WORK16 WORK16 +#if TEST_CYC_ARCHIVE +STREAM(stream_dirty8_w16, DIRTY8 WORK16) +#endif +STREAM(stream_dirty8_w32, DIRTY8 WORK32) +STREAM(stream_dirty8_w64, DIRTY8 WORK32 WORK32) +STREAM(stream_dirty8_w96, DIRTY8 WORK32 WORK32 WORK32) +#define LAST " ldr r4, [%[p], #28]\n add %[p], %[p], #32\n" +#if TEST_CYC_ARCHIVE +STREAM(stream_last_w16, LAST WORK16) +#endif +STREAM(stream_last_w32, LAST WORK32) + +/* A miss with a long way to the next one, and what else is read from its + line straight after: whether the stall is all there is when nothing + follows, and what a second word of the line, or the rest of an ldm, + waits for. */ +#define WORK64 WORK32 WORK32 +#define LINE0 " ldr r4, [%[p]]\n" +#define NEXTLINE " add %[p], %[p], #32\n" +STREAM(gap_32, LINE0 NEXTLINE WORK32) +#if TEST_CYC_ARCHIVE +STREAM(gap_64, LINE0 NEXTLINE WORK64) +#endif +STREAM(gap_128, LINE0 NEXTLINE WORK64 WORK64) +#if TEST_CYC_ARCHIVE +STREAM(gap_256, LINE0 NEXTLINE WORK64 WORK64 WORK64 WORK64) +#endif +STREAM(gap_w1, LINE0 " ldr r5, [%[p], #4]\n" NEXTLINE WORK32) +STREAM(gap_w7, LINE0 " ldr r5, [%[p], #28]\n" NEXTLINE WORK32) +#if TEST_CYC_ARCHIVE +STREAM(gap_ldm2, " ldmia %[p], { r4, r5 }\n" NEXTLINE WORK32) +#endif +STREAM(gap_ldm4, " ldmia %[p], { r4-r7 }\n" NEXTLINE WORK32) +STREAM(gap_ldm4mid, " add r5, %[p], #8\n" + " ldmia r5, { r4-r7 }\n" NEXTLINE WORK32) +STREAM(gap_str, LINE0 " str r4, [%[p]]\n" NEXTLINE WORK32) + +/* A copy, four words at a time, to 128 KB further on: line fills and + buffered writes both wanting the bus, as in a memcpy between buffers + that are not in the cache. */ +STREAM(copy_4, " ldmia %[p], { r4-r7 }\n" + " add r4, %[p], #0x20000\n" + " stmia r4, { r4-r7 }\n" + " add %[p], %[p], #16\n") + +/* The chase with work after each load, for a miss far from the last. */ +static void chase_work32(unsigned long *start, unsigned long n) +{ + asm volatile ( + " mov r4, %[s]\n" + "1:\n" + X16(" ldr r4, [r4]\n" WORK32) + " subs %[n], %[n], #1\n" + " bne 1b\n" + : [n] "+r" (n) : [s] "r" (start) : "r4", "r5", "cc", "memory"); +} + +#if TEST_CYC_ARCHIVE +/* What a codec does with a block of two channels: each buffer is filtered + in place, one after the other (pair_filter), then both are rewritten + together, four words of each per pass (pair_pass, the loop of the FLAC + decorrelation). pair_frame is all three and pair_filters the first two, + so their difference is the passes alone, as they find the cache. */ +void pair_filter(unsigned long *p, unsigned long passes); +void pair_pass(unsigned long *a, unsigned long *b, unsigned long passes); +__asm__(".text\n.align 2\n.type pair_filter, %function\n" + "pair_filter:\n" + " stmfd sp!, { r4-r7 }\n" + "1:\n" + " ldmia r0, { r4-r7 }\n" + " stmia r0!, { r4-r7 }\n" + " subs r1, r1, #1\n" + " bne 1b\n" + " ldmfd sp!, { r4-r7 }\n" + " bx lr\n" + ".type pair_pass, %function\n" + "pair_pass:\n" + " stmfd sp!, { r4-r11 }\n" + " mov r3, #0\n" + "1:\n" + " ldmia r0, { r4-r7 }\n" + " ldmia r1, { r8-r11 }\n" + " mov r4, r4, lsl r3\n" + " mov r8, r8, lsl r3\n" + " mov r5, r5, lsl r3\n" + " mov r9, r9, lsl r3\n" + " mov r6, r6, lsl r3\n" + " mov r10, r10, lsl r3\n" + " mov r7, r7, lsl r3\n" + " mov r11, r11, lsl r3\n" + " stmia r0!, { r4-r7 }\n" + " stmia r1!, { r8-r11 }\n" + " subs r2, r2, #1\n" + " bne 1b\n" + " ldmfd sp!, { r4-r11 }\n" + " bx lr\n"); + +static unsigned long *pair_a, *pair_b; +static unsigned long pair_passes; /* 16 bytes of each buffer a pass */ + +static void pair_filters(unsigned long *unused, unsigned long n) +{ + (void)unused; + while (n--) + { + pair_filter(pair_a, pair_passes); + pair_filter(pair_b, pair_passes); + } +} + +static void pair_frame(unsigned long *unused, unsigned long n) +{ + (void)unused; + while (n--) + { + pair_filter(pair_a, pair_passes); + pair_filter(pair_b, pair_passes); + pair_pass(pair_a, pair_b, pair_passes); + } +} +#endif + +/* Taken branches, one per cache line of DRAM code, each to the next, so + over more code than the cache holds every one is a fetch miss. Lines + are 16 bytes on the PP502x and 32 on the ARM9 targets. */ +#if ARM_ARCH >= 5 +#define ICHAIN_PAD "28" +#else +#define ICHAIN_PAD "12" +#endif +#define ICHAIN(name, lines) \ +static void name(unsigned long *unused, unsigned long n) \ +{ \ + (void)unused; \ + asm volatile ("1:\n" \ + " .rept " #lines "\n" \ + " b 2f\n" \ + " .space " ICHAIN_PAD "\n" \ + "2:\n" \ + " .endr\n" \ + " subs %[n], %[n], #1\n" \ + " bne 1b\n" \ + : [n] "+r" (n) : : "cc"); \ +} +#define ICHAIN_SMALL 32 /* 512 bytes, or 1 KB */ +#define ICHAIN_BIG 2048 /* 32 KB, or 64 KB */ +ICHAIN(ichain_small, 32) +ICHAIN(ichain_big, 2048) +ICHAIN(ichain_1, 1) +ICHAIN(ichain_2, 2) +ICHAIN(ichain_3, 3) +ICHAIN(ichain_4, 4) +ICHAIN(ichain_5, 5) +ICHAIN(ichain_6, 6) +ICHAIN(ichain_7, 7) +ICHAIN(ichain_8, 8) +ICHAIN(ichain_9, 9) +ICHAIN(ichain_10, 10) +ICHAIN(ichain_12, 12) +ICHAIN(ichain_16, 16) +ICHAIN(ichain_64, 64) +ICHAIN(ichain_128, 128) +ICHAIN(ichain_256, 256) + +/* Adds in a row and nothing else, the loop starting on a cache line: how + much code a loop can run through before an instruction costs more than + its cycle. */ +#define SEQ(name, count) \ +static void name(unsigned long *unused, unsigned long n) \ +{ \ + (void)unused; \ + asm volatile (" b 1f\n" \ + " .balign 32\n" \ + "1:\n" \ + " .rept " #count "\n" \ + " add r4, r4, #1\n" \ + " .endr\n" \ + " subs %[n], %[n], #1\n" \ + " bne 1b\n" \ + : [n] "+r" (n) : : "r4", "cc"); \ +} +SEQ(seq_6, 6) +SEQ(seq_14, 14) +SEQ(seq_22, 22) +SEQ(seq_30, 30) +SEQ(seq_38, 38) +SEQ(seq_46, 46) +SEQ(seq_54, 54) +SEQ(seq_62, 62) +SEQ(seq_70, 70) +SEQ(seq_78, 78) +SEQ(seq_94, 94) +SEQ(seq_126, 126) +SEQ(seq_254, 254) +SEQ(seq_510, 510) +SEQ(seq_1022, 1022) +SEQ(seq_2046, 2046) + +/* The same chain in scattered order: a full-period linear congruence over + the nodes (bytes / stride, a power of two), so consecutive loads are far + apart instead of next to each other. */ +static void make_chain_scattered(unsigned char *buf, unsigned long bytes, + unsigned long stride) +{ + unsigned long i, nodes = bytes / stride; + for (i = 0; i < nodes; i++) + *(unsigned long *)(buf + i * stride) = (unsigned long) + (buf + ((i * 1664525ul + 1013904223ul) & (nodes - 1)) * stride); +} static void make_chain(unsigned char *buf, unsigned long bytes, unsigned long stride) @@ -246,6 +671,22 @@ static void make_chain(unsigned char *buf, unsigned long bytes, #define NOW() (*(volatile unsigned long *)0x8001c0c0) /* DIGCTL usec */ #define PER_SEC 1000000ll #define TIMER_NAME "i.MX233 usec timer" +#elif CONFIG_CPU == AS3525v2 +/* No free-running counter, but the kernel tick's timer counts down from + 15000 at 1.5 MHz, so the tick and that together are one. */ +static unsigned long as3525_now(void) +{ + long t; + unsigned long v; + do { + t = *rb->current_tick; + v = *(volatile unsigned long *)0xC8040024; /* TIMER2_VALUE */ + } while (t != *rb->current_tick); + return (unsigned long)t * 15000 + (15000 - v); +} +#define NOW() as3525_now() +#define PER_SEC 1500000ll +#define TIMER_NAME "AS3525v2 tick timer, 2/3 usec" #else #define NOW() (*rb->current_tick) #define PER_SEC ((long long)HZ) @@ -261,10 +702,10 @@ static void make_chain(unsigned char *buf, unsigned long bytes, /* Hundredths of a cycle per loop iteration. The run doubles in length until the clock has moved far enough for the figure to be good to a percent. */ -static long long cpi(benchfn fn, unsigned long *p) +static long long cpi_from(benchfn fn, unsigned long *p, unsigned long n) { - unsigned long n = ITERS, t0, t1; - fn(p, 64); /* warm the caches */ + unsigned long t0, t1; + fn(p, n < 64 ? n : 64); /* warm the caches */ for (;;) { t0 = NOW(); @@ -277,6 +718,11 @@ static long long cpi(benchfn fn, unsigned long *p) return (long long)(t1 - t0) * *rb->cpu_frequency * 100 / PER_SEC / n; } +static long long cpi(benchfn fn, unsigned long *p) +{ + return cpi_from(fn, p, ITERS); +} + static int fd = -1; /* The screen keeps as many of the latest lines as fit, older ones scrolling @@ -322,12 +768,151 @@ static int per_insn(long long cyc, long long empty) return (int)((cyc - empty) / UNROLL); } -static const struct { +/* Multiplies by the size of each operand: Rm in r5 and Rs in r6, each + 14 bits, 24 bits, 31 bits or a small negative number. */ +#define RM14 " mov r5, #0x3f00\n orr r5, r5, #0xff\n" +#define RM24 " mov r5, #0x7f0000\n orr r5, r5, #0xff00\n" +#define RM31 " mvn r5, #0x80000000\n" +#define RMNEG " mvn r5, #0x3f00\n" +#define RS14 " mov r6, #0x3f00\n orr r6, r6, #0xff\n" +#define RS24 " mov r6, #0x7f0000\n orr r6, r6, #0xff00\n" +#define RS31 " mvn r6, #0x80000000\n" +#define RSNEG " mvn r6, #0x3f00\n" +#define MLA " mla r4, r5, r6, r4\n" +#define MUL " mul r4, r5, r6\n" +#define SMULL " smull r8, r9, r5, r6\n" +#define SMLAL " smlal r8, r9, r5, r6\n" +BENCH_D(b_mla_14_14, RM14 RS14, MLA) +#if TEST_CYC_ARCHIVE +BENCH_D(b_mla_24_14, RM24 RS14, MLA) +#endif +BENCH_D(b_mla_31_14, RM31 RS14, MLA) +#if TEST_CYC_ARCHIVE +BENCH_D(b_mla_n_14, RMNEG RS14, MLA) +BENCH_D(b_mla_14_24, RM14 RS24, MLA) +#endif +BENCH_D(b_mla_14_31, RM14 RS31, MLA) +BENCH_D(b_mla_14_n, RM14 RSNEG, MLA) +#if TEST_CYC_ARCHIVE +BENCH_D(b_mla_24_24, RM24 RS24, MLA) +#endif +BENCH_D(b_mla_31_31, RM31 RS31, MLA) +#if TEST_CYC_ARCHIVE +BENCH_D(b_mul_14_14, RM14 RS14, MUL) +BENCH_D(b_mul_31_14, RM31 RS14, MUL) +BENCH_D(b_mul_14_31, RM14 RS31, MUL) +BENCH_D(b_mul_31_31, RM31 RS31, MUL) +#endif +BENCH_D(b_smull_14_14, RM14 RS14, SMULL) +#if TEST_CYC_ARCHIVE +BENCH_D(b_smull_31_14, RM31 RS14, SMULL) +BENCH_D(b_smull_14_31, RM14 RS31, SMULL) +#endif +BENCH_D(b_smull_31_31, RM31 RS31, SMULL) +#if TEST_CYC_ARCHIVE +BENCH_D(b_smlal_14_14, RM14 RS14, SMLAL) +BENCH_D(b_smlal_31_31, RM31 RS31, SMLAL) +#endif + +struct row { const char *name; benchfn i, d; int per; /* instructions per X16 copy */ int alu; /* ALU cycles per copy, subtracted */ -} B[] = { +}; + +/* DRAM code only; a call row is the whole call, there and back, and the + pairs are both instructions together. */ +#define D(fn) fn##_d, fn##_d +static const struct row B2[] = { + { "bl, bx lr ", D(b_call), 1, 0 }, + { "bl,pop2 pc", D(b_callpop), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "bl,pop2 lr", D(b_callpoplr), 1, 0 }, +#endif + { "bl,pop10pc", D(b_callpop10), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "bl,popne10", D(b_callpopne), 1, 1 }, +#endif + { "bx reg ", D(b_bx), 1, 1 }, + { "ldr pc ", D(b_ldrpc), 1, 2 }, + { "bne taken ", D(b_bne), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "addne ", D(b_addne), 1, 0 }, + { "ldmne 4 ", D(b_ldmne), 1, 0 }, + { "stmne 4 ", D(b_stmne), 1, 0 }, + { "ands ", D(b_ands), 1, 0 }, +#endif + { "orr asr # ", D(b_orrasr), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "add asr Rs", D(b_addasr), 1, 0 }, + { "ldm 3 ", D(b_ldm3), 1, 0 }, +#endif + { "ldm4+use 1", D(b_ldm4first), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "ldm4+use 3", D(b_ldm4third), 1, 0 }, +#endif + { "ldm4+use 4", D(b_ldm4last), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "ldm4+mla 1", D(b_ldm4mla), 1, 0 }, + { "ldm 4 wb ", D(b_ldmwb), 1, 1 }, + { "stm 4 wb ", D(b_stmwb), 1, 1 }, + { "stm4+ldm4 ", D(b_stmldm), 1, 0 }, +#endif + { "str+ldr = ", D(b_strldrsame), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "push+pop10", D(b_push10), 1, 0 }, +#endif + { "mla+use ", D(b_mlause), 1, 0 }, + { "mla+mla Rm", D(b_mlamul), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "mul+use ", D(b_muladd), 1, 0 }, +#endif + { "smull+use ", D(b_smullacc), 1, 0 }, + { "ldr+mla ", D(b_ldrmla), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "ldr+asr Rs", D(b_ldrshift), 1, 0 }, +#endif +}; +#define NB2 ((int)(sizeof(B2) / sizeof(B2[0]))) + +/* Multiplies, named by the bits of Rm and of Rs; n is a small negative. */ +static const struct row B3[] = { + { "mla 14x14 ", D(b_mla_14_14), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "mla 24x14 ", D(b_mla_24_14), 1, 0 }, +#endif + { "mla 31x14 ", D(b_mla_31_14), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "mla nx14 ", D(b_mla_n_14), 1, 0 }, + { "mla 14x24 ", D(b_mla_14_24), 1, 0 }, +#endif + { "mla 14x31 ", D(b_mla_14_31), 1, 0 }, + { "mla 14xn ", D(b_mla_14_n), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "mla 24x24 ", D(b_mla_24_24), 1, 0 }, +#endif + { "mla 31x31 ", D(b_mla_31_31), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "mul 14x14 ", D(b_mul_14_14), 1, 0 }, + { "mul 31x14 ", D(b_mul_31_14), 1, 0 }, + { "mul 14x31 ", D(b_mul_14_31), 1, 0 }, + { "mul 31x31 ", D(b_mul_31_31), 1, 0 }, +#endif + { "smull14x14", D(b_smull_14_14), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "smull31x14", D(b_smull_31_14), 1, 0 }, + { "smull14x31", D(b_smull_14_31), 1, 0 }, +#endif + { "smull31x31", D(b_smull_31_31), 1, 0 }, +#if TEST_CYC_ARCHIVE + { "smlal14x14", D(b_smlal_14_14), 1, 0 }, + { "smlal31x31", D(b_smlal_31_31), 1, 0 }, +#endif +}; +#define NB3 ((int)(sizeof(B3) / sizeof(B3[0]))) + +static const struct row B[] = { { "add ", b_alu_i, b_alu_d, 1, 0 }, { "mov lsl Rs", b_regshift_i, b_regshift_d, 1, 0 }, { "b taken ", b_branch_i, b_branch_d, 1, 0 }, @@ -355,6 +940,12 @@ static const struct { { "ldrb Rm ", b_ldrbreg_i, b_ldrbreg_d, 2, 0 }, { "ldrh Rm ", b_ldrhreg_i, b_ldrhreg_d, 2, 0 }, { "ldr Rm+use", b_ldrreguse_i, b_ldrreguse_d, 1, 1 }, + { "addcc fail", b_addcc_i, b_addcc_d, 1, 0 }, + { "ldrcc fail", b_ldrcc_i, b_ldrcc_d, 1, 0 }, + { "strcc fail", b_strcc_i, b_strcc_d, 1, 0 }, + { "ldmcc fail", b_ldmcc_i, b_ldmcc_d, 1, 0 }, + { "mlacc fail", b_mlacc_i, b_mlacc_d, 1, 0 }, + { "bcc fail ", b_bcc_i, b_bcc_d, 1, 0 }, { "mla 14b Rs", b_mla_i, b_mla_d, 1, 0 }, { "smlal 14b ", b_smlal_i, b_smlal_d, 1, 0 }, { "mla 32b Rs", b_mla32_i, b_mla32_d, 1, 0 }, @@ -396,13 +987,975 @@ static void fmt(char *out, int v) rb->snprintf(out, 8, "%3d.%02d", v / 100, v < 0 ? -v % 100 : v % 100); } +/* Stack rows: where the stack is depends on the thread, so these are run + on the caller's thread, with code in IRAM and in DRAM. */ +static const struct { + const char *name; + benchfn i, d; +} SP[] = { + { "str [sp] ", b_spstr_i, b_spstr_d }, + { "stm 4 [sp]", b_spstm_i, b_spstm_d }, + { "ldr [sp] ", b_spldr_i, b_spldr_d }, + { "push+pop 4", b_sppush_i, b_sppush_d }, +}; +#define NSP ((int)(sizeof(SP) / sizeof(SP[0]))) + +#ifdef USE_IRAM +static long long sp_empty_i; +#endif +static long long sp_empty_d; +static int sp_res[NSP][2]; +static unsigned long sp_addr; +static volatile bool sp_done; + +static void stack_rows(void) +{ + int k; + asm volatile ("mov %0, sp" : "=r" (sp_addr)); + for (k = 0; k < NSP; k++) + { +#ifdef USE_IRAM + sp_res[k][0] = per_insn(cpi(SP[k].i, dram_buf), sp_empty_i); +#endif + sp_res[k][1] = per_insn(cpi(SP[k].d, dram_buf), sp_empty_d); + } + sp_done = true; +} + +static void say_stack_rows(const char *thread) +{ + int k; + say("stack rows, %s thread, sp %08lx", thread, sp_addr); +#ifdef USE_IRAM + say("cyc/insn code:IRAM DRAM"); +#endif + for (k = 0; k < NSP; k++) + { + char a[8], b[8]; + fmt(b, sp_res[k][1]); +#ifdef USE_IRAM + fmt(a, sp_res[k][0]); + say("%s %s %s", SP[k].name, a, b); +#else + (void)a; + say("%s %s", SP[k].name, b); +#endif + } +} + +/* A real kernel, a stage at a time: libtta's hybrid_filter (filter_arm.S), + its error > 0 path, cut off after each stage in turn, to find where a + whole function costs more than its instructions do one by one. Each + stage is the ones before it and: + 0 the frame: push and pop of ten registers, through the pc + 1 the three-word load of the state, five adds, the branches + 2 ldm 4, ldm 4, four adds, stm 4: the first weights + 3 ldm 4 and four mla + 4 stage 2 again for the next four weights + 5 stage 3 again + 6 ldr, ldr, add with a register shift, str, ldr, add, ands, stm 2 + 7 mov, four orr with a shift, three shifts, three subs + 8 the two conditional stm 4 and the conditional return: the whole + function, shifting its delay lines every sixteenth call */ +#define TCF_ADD4 " add r5, r5, r9\n add r6, r6, r10\n" \ + " add r7, r7, r11\n add r8, r8, r12\n" +#define TCF_MLA4 " mla lr, r5, r9, lr\n mla lr, r6, r10, lr\n" \ + " mla lr, r7, r11, lr\n mla lr, r8, r12, lr\n" +__asm__(".text\n.align 2\n" + ".macro TCF name, stage, sub=0, step=4\n" + ".type \\name, %function\n" + "\\name:\n" + " stmdb sp!, { r4-r12, lr }\n" + ".if \\stage >= 1\n" + " ldmia r0, { r5, r6, lr }\n" + " add r2, r0, #148\n" + " add r3, r0, #52\n" + " add r4, r0, #20\n" + " add r2, r2, r5\n" + " add r3, r3, r5\n" + " cmp r6, #0\n" + " bmi 9f\n" + " bne 1f\n" + " b 9f\n" + "1:\n" + ".endif\n" + ".if \\stage >= 2\n" + " ldmia r4, { r5, r6, r7, r8 }\n" + " ldmia r3!, { r9, r10, r11, r12 }\n" + TCF_ADD4 + " stmia r4!, { r5, r6, r7, r8 }\n" + ".endif\n" + ".if \\stage >= 3\n" + " ldmia r2!, { r9, r10, r11, r12 }\n" + TCF_MLA4 + ".endif\n" + ".if \\stage >= 4\n" + " ldmia r4, { r5, r6, r7, r8 }\n" + " ldmia r3!, { r9, r10, r11, r12 }\n" + TCF_ADD4 + " stmia r4!, { r5, r6, r7, r8 }\n" + ".endif\n" + ".if \\stage >= 5\n" + " ldmia r2!, { r9, r10, r11, r12 }\n" + TCF_MLA4 + ".endif\n" + ".irp k, 1, 2, 3, 4, 5, 6, 7, 8\n" + ".if (\\stage >= 6) | (\\sub >= \\k)\n" + ".if \\k == 1\n ldr r5, [r1]\n.endif\n" + ".if \\k == 2\n ldr r6, [r0, #12]\n.endif\n" + ".if \\k == 3\n add lr, r5, lr, asr r6\n.endif\n" + ".if \\k == 4\n str lr, [r1]\n.endif\n" + ".if \\k == 5\n ldr r1, [r0]\n.endif\n" + ".if \\k == 6\n add r1, r1, #\\step\n.endif\n" + ".if \\k == 7\n ands r1, r1, #63\n.endif\n" + ".if \\k == 8\n stmia r0, { r1, r5 }\n.endif\n" + ".endif\n" + ".endr\n" + ".if \\stage >= 7\n" + " mov r4, #1\n" + " orr r5, r4, r9, asr #30\n" + " orr r6, r4, r10, asr #30\n" + " orr r7, r4, r11, asr #30\n" + " orr r8, r4, r12, asr #30\n" + " mov r6, r6, lsl #1\n" + " mov r7, r7, lsl #1\n" + " mov r8, r8, lsl #2\n" + " sub r12, lr, r12\n" + " sub r11, r12, r11\n" + " sub r10, r11, r10\n" + ".endif\n" + ".if \\stage >= 8\n" + " stmneda r2, { r10, r11, r12, lr }\n" + " stmneda r3, { r5, r6, r7, r8 }\n" + " ldmnefd sp!, { r4-r12, pc }\n" + " add r2, r0, #212\n" + " ldmia r2, { r1, r3, r4 }\n" + " sub r2, r2, #64\n" + " stmia r2, { r1, r3, r4, r9-r12, lr }\n" + " add r9, r0, #116\n" + " ldmia r9, { r1, r2, r3, r4 }\n" + " sub r9, r9, #64\n" + " stmia r9, { r1-r8 }\n" + ".endif\n" + "9:\n" + " ldmfd sp!, { r4-r12, pc }\n" + ".endm\n" + "TCF tcf_0, 0\nTCF tcf_1, 1\nTCF tcf_2, 2\nTCF tcf_3, 3\n" + "TCF tcf_4, 4\nTCF tcf_5, 5\nTCF tcf_6, 6\nTCF tcf_7, 7\n" + "TCF tcf_8, 8\n" + "TCF tcf_61, 5, 1\nTCF tcf_62, 5, 2\nTCF tcf_63, 5, 3\n" + "TCF tcf_64, 5, 4\nTCF tcf_65, 5, 5\nTCF tcf_66, 5, 6\n" + "TCF tcf_67, 5, 7\nTCF tcf_6s, 6, 0, 0\nTCF tcf_7s, 7, 0, 0\n" + /* Stage 5 again at each word of a cache line, and frames holding + only adds, as long as the filter's stages are. */ + ".irp k, 0, 1, 2, 3, 4, 5, 6, 7\n" + ".balign 32\n" + ".if \\k\n" + ".space 4 * \\k\n" + ".endif\n" + "TCF tcf_5c\\k, 5\n" + ".endr\n" + ".macro TCP name, n\n" + ".type \\name, %function\n" + "\\name:\n" + " stmdb sp!, { r4-r12, lr }\n" + ".rept \\n\n" + " add r4, r4, #1\n" + ".endr\n" + " ldmfd sp!, { r4-r12, pc }\n" + ".endm\n" + "TCP tcp_40, 40\nTCP tcp_60, 60\nTCP tcp_70, 70\n" + "TCP tcp_80, 80\nTCP tcp_100, 100\nTCP tcp_120, 120\n" + ".type tcf_ret, %function\n" + "tcf_ret:\n" + " bx lr\n"); +int tcf_5c0(int *, int *); int tcf_5c1(int *, int *); +int tcf_5c2(int *, int *); int tcf_5c3(int *, int *); +int tcf_5c4(int *, int *); int tcf_5c5(int *, int *); +int tcf_5c6(int *, int *); int tcf_5c7(int *, int *); +int tcp_40(int *, int *); int tcp_60(int *, int *); int tcp_70(int *, int *); +int tcp_80(int *, int *); int tcp_100(int *, int *); +int tcp_120(int *, int *); +typedef int (*tcf_fn)(int *fs, int *in); +int tcf_0(int *, int *); int tcf_1(int *, int *); int tcf_2(int *, int *); +int tcf_3(int *, int *); int tcf_4(int *, int *); int tcf_5(int *, int *); +int tcf_6(int *, int *); int tcf_7(int *, int *); int tcf_8(int *, int *); +int tcf_61(int *, int *); int tcf_62(int *, int *); int tcf_63(int *, int *); +int tcf_64(int *, int *); int tcf_65(int *, int *); int tcf_66(int *, int *); +int tcf_67(int *, int *); int tcf_6s(int *, int *); int tcf_7s(int *, int *); +int tcf_ret(int *, int *); + +/* The filter's state, as fltst lays it out: index, error, round, shift, + a spare word, then qm[8], dx[24] and dl[24]. */ +static int tcf_buf[61 + 8] __attribute__((aligned(32))); +static int *tcf_fs = tcf_buf; /* the state, somewhere in tcf_buf */ +static int tcf_in[64]; +static tcf_fn tcf_now; + +static void tcf_loop(unsigned long *unused, unsigned long n) +{ + tcf_fn fn = tcf_now; + int *fs = tcf_fs; + unsigned long i; + int v; + (void)unused; + for (i = 0; i < n; i++) + { + v = tcf_in[i & 63]; + fn(fs, &v); + } +} + +/* Hundredths of a cycle a call, the function's own return included. */ +static long long tcf_cycles(tcf_fn fn) +{ + int i; + rb->memset(tcf_buf, 0, sizeof(tcf_buf)); + tcf_fs[1] = 1; /* error > 0 */ + tcf_fs[2] = 512; /* round */ + tcf_fs[3] = 10; /* shift */ + for (i = 0; i < 64; i++) + tcf_in[i] = 500 + i * 37 % 1000; + tcf_now = fn; + return cpi(tcf_loop, NULL); +} + +static void say_filter_rows(void) +{ + /* By the rows above: the frame 24, then 13, 16, 12, 16, 12, 12 and 11, + and 9.75 for the last, the delay lines' shift included. On an + ARM926 the later stages come out a tenth over, which is the cycle a + line that "adds in a loop" measures: with its caller the function + is more than eight lines of code. The archive has stage 6 an + instruction at a time (6.1 to 6.7, 6 being all eight), and stages + 6 and 7 with the index left where it is (6s, 7s). */ + static const struct { const char *name; tcf_fn fn; int want; } T[] = { + { "0 ", tcf_0, 2400 }, { "1 ", tcf_1, 3700 }, + { "2 ", tcf_2, 5300 }, { "3 ", tcf_3, 6500 }, + { "4 ", tcf_4, 8100 }, { "5 ", tcf_5, 9300 }, +#if TEST_CYC_ARCHIVE + { "6.1", tcf_61, 9400 }, { "6.2", tcf_62, 9500 }, + { "6.3", tcf_63, 9800 }, { "6.4", tcf_64, 9900 }, + { "6.5", tcf_65, 10000 }, { "6.6", tcf_66, 10200 }, + { "6.7", tcf_67, 10300 }, +#endif + { "6 ", tcf_6, 10500 }, + { "7 ", tcf_7, 11600 }, { "8 ", tcf_8, 12575 }, +#if TEST_CYC_ARCHIVE + { "6s ", tcf_6s, 10500 }, { "7s ", tcf_7s, 11600 }, +#endif + }; + long long base = tcf_cycles(tcf_ret); + unsigned k; + int last = 0; + say("tta filter, state at %08lx", (unsigned long)tcf_fs); + say(" stage cyc step by rows"); + for (k = 0; k < sizeof(T) / sizeof(T[0]); k++) + { + char a[8], b[8], c[8]; + int v = (int)(tcf_cycles(T[k].fn) - base) + 300; + fmt(a, v); + fmt(b, v - last); + fmt(c, T[k].want); + say(" %s %s %s %s", T[k].name, a, b, c); + last = v; + } +} + +#if TEST_CYC_ARCHIVE +/* Where the filter's cost depends on something other than its + instructions: stage 5 with its state at each word of a cache line, the + same code at each word of a line, and frames of nothing but adds. */ +static void say_filter_layout(void) +{ + static const tcf_fn code[] = { tcf_5c0, tcf_5c1, tcf_5c2, tcf_5c3, + tcf_5c4, tcf_5c5, tcf_5c6, tcf_5c7 }; + static const struct { tcf_fn fn; int n; } pad[] = { + { tcp_40, 40 }, { tcp_60, 60 }, { tcp_70, 70 }, { tcp_80, 80 }, + { tcp_100, 100 }, { tcp_120, 120 } }; + long long base = tcf_cycles(tcf_ret); + unsigned k; + char a[8], b[8]; + say("stage 5 (93) and 4 (81), state at word"); + for (k = 0; k < 8; k++) + { + tcf_fs = tcf_buf + k; + fmt(a, (int)(tcf_cycles(tcf_5) - base) + 300); + fmt(b, (int)(tcf_cycles(tcf_4) - base) + 300); + say(" %u %s %s", k, a, b); + } + tcf_fs = tcf_buf; + say("stage 5 (93), code at word"); + for (k = 0; k < 8; k++) + { + fmt(a, (int)(tcf_cycles(code[k]) - base) + 300); + say(" %u %s at %08lx", k, a, (unsigned long)code[k]); + } + say("frame of adds cyc by rows"); + for (k = 0; k < sizeof(pad) / sizeof(pad[0]); k++) + { + fmt(a, (int)(tcf_cycles(pad[k].fn) - base) + 300); + fmt(b, (24 + pad[k].n) * 100); + say(" %3d adds %s %s", pad[k].n, a, b); + } +} +#endif + +/* The empty loop, by where its code and its data are. */ +static long long e_d_d; +#ifdef USE_IRAM +static long long e_i_i, e_i_d, e_d_i; +#endif + +#ifndef TICK_TIMED +#if TEST_CYC_ARCHIVE +/* Which lines a miss evicts. The 16 KB at the start of the buffer is + read until the data cache holds it, then a stream of misses is run + through memory elsewhere, a load a line, and the first 16 KB is read + once more and timed: what share of its lines are still there. The + stream's loop is padded with 0 to 5 adds, since a victim chosen by a + free-running counter would depend on the loop's length where a random + one would not: at random, a quarter of what is left goes with every 128 + lines, leaving 75%, 32%, and after 2048 lines nothing. */ +#define EVICT(name, pad) \ +static void name(unsigned long *p, unsigned long n) \ +{ \ + asm volatile ("1:\n" \ + " ldr r4, [%[p]], #32\n" \ + pad \ + " subs %[n], %[n], #1\n" \ + " bne 1b\n" \ + : [n] "+&r" (n), [p] "+&r" (p) : : "r4", "r5", "cc"); \ +} +#define PAD1 " add r5, r5, #1\n" +EVICT(evict_0, "") +EVICT(evict_1, PAD1) +EVICT(evict_2, PAD1 PAD1) +EVICT(evict_3, PAD1 PAD1 PAD1) +EVICT(evict_5, PAD1 PAD1 PAD1 PAD1 PAD1) + +/* Timer counts for one read of the first 16 KB, after `lines` misses made + by fn elsewhere; fn NULL leaves it as it is. */ +static unsigned long evict_pass(unsigned char *big, benchfn fn, + unsigned long lines) +{ + unsigned long t; + int i; + for (i = 0; i < 12; i++) + evict_0((unsigned long *)big, 512); + if (fn) + fn((unsigned long *)(big + 65536), lines); + t = NOW(); + evict_0((unsigned long *)big, 512); + return NOW() - t; +} + +/* Which way a miss takes, miss after miss. Four lines are loaded into + one set until they fill it; then some new lines are loaded into that + set, with misses in other sets in between; then the four are read again + and timed, each followed by enough adds for a miss to cost its stall + alone. A victim picked at random each time keeps 75%, 56%, 42% and 32% + of the four after 1 to 4 new lines whatever comes between; one that + takes the ways in turn keeps 75%, 50%, 25% and none. */ +void evict_reload(unsigned char *line0); +__asm__(".text\n.align 2\n.type evict_reload, %function\n" + "evict_reload:\n" + " mov r2, #4\n" + "1:\n" + " ldr r1, [r0]\n" + " add r0, r0, #4096\n" + " .rept 32\n" + " add r3, r3, #1\n" + " .endr\n" + " subs r2, r2, #1\n" + " bne 1b\n" + " bx lr\n"); + +static unsigned long evict_set_trial(unsigned char *big, int news, int gap) +{ + unsigned long total = 0, t, fill = 0; + int set, k, round, i; + for (set = 0; set < 128; set++) + { + unsigned char *line = big + set * 32; + for (round = 0; round < 8; round++) + for (k = 0; k < 4; k++) + (void)*(volatile unsigned long *)(line + k * 4096); + for (k = 0; k < news; k++) + { + (void)*(volatile unsigned long *)(line + (4 + k) * 4096); + for (i = 0; i < gap; i++, fill++) + (void)*(volatile unsigned long *)(big + 131072 + + ((set + 1 + fill % 126) & 127) * 32 + + (fill / 126 % 16) * 4096); + } + t = NOW(); + evict_reload(line); + total += NOW() - t; + } + return total; +} + +static void say_evict_set_rows(void) +{ + static const unsigned char gaps[] = { 0, 1, 2, 3, 7, 127 }; + size_t bufsize; + unsigned char *big = rb->plugin_get_buffer(&bufsize); + unsigned long hit = 0, miss = 0; + unsigned g, n, r; + big = (unsigned char *)(((unsigned long)big + 31) & ~31ul); + if (bufsize < 262144) + return; + for (r = 0; r < 8; r++) + { + hit += evict_set_trial(big, 0, 0); + miss += evict_set_trial(big, 20, 0); + } + say("4 lines of a set: kept %lu, gone %lu counts", hit / 8, miss / 8); + say("kept, %%, after new lines in the set"); + say(" between 1 2 3 4 8"); + for (g = 0; g < sizeof(gaps); g++) + { + static const unsigned char news[] = { 1, 2, 3, 4, 8 }; + int kept[5]; + for (n = 0; n < 5; n++) + { + unsigned long t = 0; + for (r = 0; r < 8; r++) + t += evict_set_trial(big, news[n], gaps[g]); + kept[n] = miss > hit + ? (int)(100 - 100 * ((long long)t - hit) / (miss - hit)) : 0; + } + say(" %3d %4d %4d %4d %4d %4d", gaps[g], kept[0], kept[1], + kept[2], kept[3], kept[4]); + } +} +#endif + +/* The same question asked more sharply: a line is loaded, then one to + five more into its set, each a miss with the bus idle before it (32 + adds, plus 0 to 7 more to move the misses in time), and the first is + read again. Chosen at random it survives 75%, 56%, 42%, 32% and 24% of + the time, whatever the spacing. With the ways taken in turn it always + survives three more and never four. With the victim read off a + free-running counter it depends on the spacing, a row at a time. */ +#define EVICTN(name, pad) \ +void name(unsigned char *line, unsigned long n); +#define EVICTN_ASM(name, pad) \ + ".type " #name ", %function\n" \ + #name ":\n" \ + "1:\n" \ + " ldr r2, [r0]\n" \ + " add r0, r0, #4096\n" \ + " .rept 32 + " #pad "\n" \ + " add r3, r3, #1\n" \ + " .endr\n" \ + " subs r1, r1, #1\n" \ + " bne 1b\n" \ + " bx lr\n" +EVICTN(evictn_0, 0) EVICTN(evictn_1, 1) EVICTN(evictn_2, 2) +EVICTN(evictn_3, 3) EVICTN(evictn_4, 4) EVICTN(evictn_5, 5) +EVICTN(evictn_6, 6) EVICTN(evictn_7, 7) +__asm__(".text\n.align 2\n" + EVICTN_ASM(evictn_0, 0) EVICTN_ASM(evictn_1, 1) + EVICTN_ASM(evictn_2, 2) EVICTN_ASM(evictn_3, 3) + EVICTN_ASM(evictn_4, 4) EVICTN_ASM(evictn_5, 5) + EVICTN_ASM(evictn_6, 6) EVICTN_ASM(evictn_7, 7)); + +/* Timer counts to read back the first of 1 + more lines loaded into each + set in turn. The lines come from 56 that share the set, taken round, + so that nearly every load is a miss. */ +static unsigned long evict_lag_trial(unsigned char *big, + void (*fn)(unsigned char *, + unsigned long), + int more) +{ + static unsigned tag; + unsigned long total = 0, t; + int set; + for (set = 0; set < 128; set++) + { + unsigned char *line; + if (tag + 1 + more > 56) + tag = 0; + line = big + set * 32 + tag * 4096; + fn(line, 1 + more); + t = NOW(); + evictn_0(line, 1); + total += NOW() - t; + tag += 1 + more; + } + return total; +} + +static void say_evict_lag_rows(void) +{ + static void (*const fn[])(unsigned char *, unsigned long) = { + evictn_0, evictn_1, evictn_2, evictn_3, + evictn_4, evictn_5, evictn_6, evictn_7 }; + size_t bufsize; + unsigned char *big = rb->plugin_get_buffer(&bufsize); + unsigned long hit = 0, miss = 0; + unsigned d, k, r; + big = (unsigned char *)(((unsigned long)big + 31) & ~31ul); + if (bufsize < 56 * 4096 + 8192) + return; + for (r = 0; r < 16; r++) + { + hit += evict_lag_trial(big, evictn_0, 0); + miss += evict_lag_trial(big, evictn_0, 16); + } + say("first line back: kept %lu, gone %lu counts", hit / 16, miss / 16); + say("first line kept, %%, after more misses in its set"); + say(" pad 1 2 3 4 5"); + for (d = 0; d < 8; d++) + { + int kept[5]; + for (k = 0; k < 5; k++) + { + unsigned long t = 0; + for (r = 0; r < 16; r++) + t += evict_lag_trial(big, fn[d], k + 1); + kept[k] = miss > hit + ? (int)(100 - 100 * ((long long)t - hit) / (miss - hit)) : 0; + } + say(" %u %4d %4d %4d %4d %4d", d, kept[0], kept[1], kept[2], + kept[3], kept[4]); + } +} + +#if TEST_CYC_ARCHIVE +static void say_evict_rows(void) +{ + static const struct { benchfn fn; int pad; } E[] = { + { evict_0, 0 }, { evict_1, 1 }, { evict_2, 2 }, { evict_3, 3 }, + { evict_5, 5 } }; + static const unsigned short lines[] = { 128, 512, 2048 }; + size_t bufsize; + unsigned char *big = rb->plugin_get_buffer(&bufsize); + unsigned long hit = 0, miss = 0, t; + unsigned k, j, r; + big = (unsigned char *)(((unsigned long)big + 31) & ~31ul); + if (bufsize < 65536 + 2048 * 32 + 64) + return; + for (r = 0; r < 16; r++) + { + hit += evict_pass(big, NULL, 0); + miss += evict_pass(big, evict_0, 4096); + } + say("16 KB read: cached %lu, evicted %lu counts", hit / 16, miss / 16); + say("lines kept, %%, after misses elsewhere"); + say(" pad 128 512 2048 lines"); + for (k = 0; k < sizeof(E) / sizeof(E[0]); k++) + { + int kept[3]; + for (j = 0; j < 3; j++) + { + t = 0; + for (r = 0; r < 16; r++) + t += evict_pass(big, E[k].fn, lines[j]); + kept[j] = miss > hit + ? (int)(100 - 100 * ((long long)t - hit) / (miss - hit)) : 0; + } + say(" %d %4d %4d %4d", E[k].pad, kept[0], kept[1], kept[2]); + } +} +#endif +#endif + +/* Misses with room after them, and back to back, in the memory at big: + the plugin buffer, which is DRAM, or on an AS3525v2 the megabyte of RAM + inside the SoC, where codecs are loaded with all their data. */ +static void say_miss_gap_rows(const char *where, unsigned char *big, + size_t bufsize) +{ + static const struct { const char *name; benchfn fn; } G[] = { + { "+32 alu ", gap_32 }, +#if TEST_CYC_ARCHIVE + { "+64 alu ", gap_64 }, +#endif + { "+128 alu ", gap_128 }, +#if TEST_CYC_ARCHIVE + { "+256 alu ", gap_256 }, +#endif + { "ldr w1, +32 ", gap_w1 }, + { "ldr w7, +32 ", gap_w7 }, +#if TEST_CYC_ARCHIVE + { "ldm 2, +32 ", gap_ldm2 }, +#endif + { "ldm 4, +32 ", gap_ldm4 }, + { "ldm w2-5,+32", gap_ldm4mid }, + { "str w0, +32 ", gap_str }, + }; + static const struct { const char *name; benchfn fn; } P[] = { + { "line, no gap", stream_line }, + { "ldm 4 stream", stream_ldm4 }, + { "dirty 4 ", stream_dirty4 }, + { "dirty 8 ", stream_dirty8 }, + { "str stream ", wstream_word }, + { "str a line ", wstream_line }, + { "stm 4 stream", wstream_stm4 }, + { "copy 4 words", copy_4 }, + }; + unsigned long large; + unsigned k; + char a[8], b[8], c[8]; + int hit, miss; + large = (bufsize > 262144 ? 262144 : bufsize) & ~511ul; + say("%s at %08lx", where, (unsigned long)big); + say("miss, then cached %luKB extra", large / 1024); + for (k = 0; k < sizeof(G) / sizeof(G[0]); k++) + { + sbuf = (unsigned long *)big; + sbytes = 1024; + hit = per_insn(cpi_from(G[k].fn, NULL, 2048), e_d_d); + sbytes = large; + miss = per_insn(cpi_from(G[k].fn, NULL, 2048), e_d_d); + fmt(a, hit); + fmt(b, miss); + fmt(c, miss - hit); + say(" %s %s %s %s", G[k].name, a, b, c); + } + if (bufsize >= 262144) + { + make_chain(big, 1024, 32); + hit = per_insn(cpi_from(chase_work32, (unsigned long *)big, 2048), + e_d_d); + make_chain(big, 262144, 32); + miss = per_insn(cpi_from(chase_work32, (unsigned long *)big, 2048), + e_d_d); + fmt(a, hit); + fmt(b, miss); + fmt(c, miss - hit); + say(" chase, +32 %s %s %s", a, b, c); + make_chain_scattered(big, 262144, 32); + miss = per_insn(cpi_from(chase_work32, (unsigned long *)big, 2048), + e_d_d); + fmt(b, miss); + fmt(c, miss - hit); + say(" scattered %s %s %s", a, b, c); + } + /* Back to back. The first kilobyte is read before each row, so the + store rows' first column is of stores to lines that are cached. */ + for (k = 0; k < sizeof(P) / sizeof(P[0]); k++) + { + sbuf = (unsigned long *)big; + sbytes = 1024; + stream_word(NULL, 64); + hit = per_insn(cpi_from(P[k].fn, NULL, 2048), e_d_d); + /* The copy reads the first half of the buffer and writes the + second. */ + sbytes = P[k].fn == copy_4 ? 131072 : large; + miss = per_insn(cpi_from(P[k].fn, NULL, 2048), e_d_d); + fmt(a, hit); + fmt(b, miss); + fmt(c, miss - hit); + say(" %s %s %s %s", P[k].name, a, b, c); + } +} + +static void say_miss_rows_everywhere(void) +{ + size_t bufsize; + unsigned char *big = rb->plugin_get_buffer(&bufsize); + big = (unsigned char *)(((unsigned long)big + 31) & ~31ul); + say_miss_gap_rows("DRAM", big, bufsize - 32); +#if CONFIG_CPU == AS3525v2 + /* The SoC's own RAM is mapped after the DRAM; its first 32 KB is the + firmware's and the rest is where a codec goes, free while nothing + plays. */ + rb->audio_stop(); + say_miss_gap_rows("SoC RAM", (unsigned char *)0x30000000 + + MEMORYSIZE * 0x100000 + 0x40000, 0x80000); +#endif +} + +#if ARM_ARCH >= 5 +/* Fetch misses in any memory: the code is written where it is to run. A + loop of one taken branch a line, or of lines of eight adds, over 4 KB, + which the cache holds, and over 64 KB, where every line is a miss. */ +static void make_code(unsigned long *code, int lines, bool adds) +{ + unsigned long *w = code; + int k, j; + for (k = 0; k < lines; k++, w += 8) + { + for (j = 0; j < 8; j++) + w[j] = 0xe2800001; /* add r0, r0, #1 */ + if (!adds) + w[0] = 0xea000006; /* b the next line */ + } + w[0] = 0xe2511001; /* subs r1, r1, #1 */ + w[1] = 0x1a000000 /* bne the first line */ + | ((unsigned long)(code - (w + 1) - 2) & 0xfffffful); + w[2] = 0xe12fff1e; /* bx lr */ + rb->commit_dcache(); + rb->commit_discard_idcache(); +} + +static void say_code_miss_rows(const char *where, unsigned long *code) +{ + static const char *const name[] = { "a branch a line", "8 adds a line " }; + int v; + char a[8], b[8], c[8]; + say("%s at %08lx, code", where, (unsigned long)code); + say("cycles a line 4KB 64KB extra"); + for (v = 0; v < 2; v++) + { + int hit, miss; + make_code(code, 128, v); + hit = (int)(cpi_from((benchfn)code, NULL, 512) / 128); + make_code(code, 2048, v); + miss = (int)(cpi_from((benchfn)code, NULL, 64) / 2048); + fmt(a, hit); + fmt(b, miss); + fmt(c, miss - hit); + say(" %s %s %s %s", name[v], a, b, c); + } +} + +#if CONFIG_CPU == AS3525v2 +/* The copy of copy_4, from one memory to the other: whether a fill in one + waits for the writes still to go out to the other. */ +static unsigned long sdelta; +static void copy_across(unsigned long *unused, unsigned long n) +{ + unsigned long *p = sbuf, *end = sbuf + sbytes / 4; + (void)unused; + asm volatile ("1:\n" + X16(" ldmia %[p], { r4-r7 }\n" + " add r8, %[p], %[d]\n" + " stmia r8, { r4-r7 }\n" + " add %[p], %[p], #16\n") + " cmp %[p], %[end]\n" + " movhs %[p], %[base]\n" + " subs %[n], %[n], #1\n" + " bne 1b\n" + : [n] "+&r" (n), [p] "+&r" (p) + : [end] "r" (end), [base] "r" (sbuf), [d] "r" (sdelta) + : "r4", "r5", "r6", "r7", "r8", "cc", "memory"); +} + +static void say_copy_across(const char *name, unsigned long *from, + unsigned long *to) +{ + char a[8], b[8], c[8]; + int hit, miss; + sbuf = from; + sdelta = (unsigned long)to - (unsigned long)from; + sbytes = 1024; + stream_word(NULL, 64); + hit = per_insn(cpi_from(copy_across, NULL, 2048), e_d_d); + sbytes = 131072; + miss = per_insn(cpi_from(copy_across, NULL, 2048), e_d_d); + fmt(a, hit); + fmt(b, miss); + fmt(c, miss - hit); + say(" %s %s %s %s", name, a, b, c); +} +#endif + +/* Tremor's window loop, which reads two buffers, one upwards and one + downwards, and stores a word to each of two more, none of them cached: + args is the buffer, a quarter of its size and a small table. And the + two streams of stores alone. Each pass is 16 times the body. */ +#define VBODY_STORES \ + " str r5, [r9, #-4]!\n" \ + " str r5, [r6], #4\n" +#define VBODY_WINDOW \ + " ldr r0, [r7], #4\n" \ + " ldr r1, [ip, #-4]!\n" \ + " ldr r2, [r8]\n" \ + " ldr r3, [r8, #4]\n" \ + " smull r4, r5, r0, r2\n" \ + " smlal r4, r5, r1, r3\n" \ + " rsb r2, r2, #0\n" \ + " mov r5, r5, lsl #1\n" \ + " str r5, [r9, #-4]!\n" \ + " smull r4, r5, r0, r3\n" \ + " smlal r4, r5, r1, r2\n" \ + " mov r5, r5, lsl #1\n" \ + " str r5, [r6], #4\n" +#define VLOOP(name, body, step) \ +void name(unsigned long *args, unsigned long n); \ +asm (" .pushsection .text." #name ", \"ax\", %progbits\n" \ + " .align 2\n" \ + " .type " #name ", %function\n" \ + #name ":\n" \ + " stmfd sp!, { r4-r11, lr }\n" \ + " mov r11, r0\n" \ + " mov r10, r1\n" \ + " ldr r8, [r11, #8]\n" \ + "0:\n" \ + " ldr r7, [r11]\n" \ + " ldr r3, [r11, #4]\n" \ + " add ip, r7, r3, lsl #1\n" \ + " add r6, ip, r3\n" \ + " mov r9, r6\n" \ + " add lr, r7, r3\n" \ + "1:\n" \ + X16(body) \ + step \ + " subs r10, r10, #1\n" \ + " beq 2f\n" \ + " cmp r7, lr\n" \ + " blo 1b\n" \ + " b 0b\n" \ + "2:\n" \ + " ldmfd sp!, { r4-r11, pc }\n" \ + " .popsection\n"); +VLOOP(vloop_stores, VBODY_STORES, " add r7, r7, #64\n") +VLOOP(vloop_window, VBODY_WINDOW, "") + +static void say_window_rows(const char *where, unsigned long *big, + size_t bufsize) +{ + static const struct { const char *name; benchfn fn; } V[] = { + { "2 str streams", vloop_stores }, + { "window loop ", vloop_window }, + }; + static unsigned long table[2] = { 0x3fffffff, 0x12345678 }; + unsigned long args[3]; + unsigned long large = (bufsize > 262144 ? 262144 : bufsize) & ~1023ul; + unsigned k; + char a[8], b[8], c[8]; + say("%s at %08lx", where, (unsigned long)big); + say("two streams out cached %luKB extra", large / 1024); + args[0] = (unsigned long)big; + args[2] = (unsigned long)table; + for (k = 0; k < sizeof(V) / sizeof(V[0]); k++) + { + int hit, miss; + sbuf = big; + sbytes = 1024; + stream_word(NULL, 64); + args[1] = 256; + hit = per_insn(cpi_from(V[k].fn, args, 2048), e_d_d); + args[1] = large / 4; + miss = per_insn(cpi_from(V[k].fn, args, 2048), e_d_d); + fmt(a, hit); + fmt(b, miss); + fmt(c, miss - hit); + say(" %s %s %s %s", V[k].name, a, b, c); + } +} + +static void say_window_rows_everywhere(void) +{ + size_t bufsize; + unsigned char *big = rb->plugin_get_buffer(&bufsize); + big = (unsigned char *)(((unsigned long)big + 31) & ~31ul); + say_window_rows("DRAM", (unsigned long *)big, bufsize - 32); +#if CONFIG_CPU == AS3525v2 + rb->audio_stop(); + say_window_rows("SoC RAM", (unsigned long *)(0x30000000 + + MEMORYSIZE * 0x100000 + 0x40000), 0x80000); +#endif +} + +static void say_code_miss_rows_everywhere(void) +{ + size_t bufsize; + unsigned char *big = rb->plugin_get_buffer(&bufsize); + big = (unsigned char *)(((unsigned long)big + 31) & ~31ul); + if (bufsize >= 0x10100) + say_code_miss_rows("DRAM", (unsigned long *)big); +#if CONFIG_CPU == AS3525v2 + rb->audio_stop(); + say_code_miss_rows("SoC RAM", (unsigned long *)(0x30000000 + + MEMORYSIZE * 0x100000 + 0x40000)); + if (bufsize >= 0x20100) + { + unsigned long *soc = (unsigned long *)(0x30000000 + + MEMORYSIZE * 0x100000 + 0x40000); + say("copy 4 words cached 128KB extra"); + say_copy_across("DRAM to SoC ", (unsigned long *)big, soc); + say_copy_across("SoC to DRAM ", soc, (unsigned long *)big); + } +#endif +} +#endif + +/* How much code a loop holds against what its instructions cost: loops of + adds filling one cache line to 256 of them, and of one taken branch a + line. */ +static void say_code_size_rows(void) +{ + static const struct { benchfn fn; int n; } S[] = { + { seq_6, 6 }, { seq_14, 14 }, { seq_22, 22 }, { seq_30, 30 }, + { seq_38, 38 }, { seq_46, 46 }, { seq_54, 54 }, { seq_62, 62 }, + { seq_70, 70 }, { seq_78, 78 }, { seq_94, 94 }, + { seq_126, 126 }, { seq_254, 254 }, { seq_510, 510 }, + { seq_1022, 1022 }, { seq_2046, 2046 } }; + static const struct { benchfn fn; int n; } C[] = { + { ichain_1, 1 }, { ichain_2, 2 }, { ichain_3, 3 }, { ichain_4, 4 }, + { ichain_5, 5 }, { ichain_6, 6 }, { ichain_7, 7 }, { ichain_8, 8 }, + { ichain_9, 9 }, { ichain_10, 10 }, { ichain_12, 12 }, + { ichain_16, 16 }, { ichain_small, 32 }, + { ichain_64, 64 }, { ichain_128, 128 }, { ichain_256, 256 } }; + unsigned k; + char a[8], b[8]; + say("adds in a loop lines cyc/add over"); + for (k = 0; k < sizeof(S) / sizeof(S[0]); k++) + { + long long c = cpi_from(S[k].fn, NULL, 3200000ul / S[k].n) - e_d_d; + fmt(a, (int)(c / S[k].n)); + fmt(b, (int)(c - S[k].n * 100)); + say(" %4d adds %3d %s %s", S[k].n, (S[k].n + 2) / 8, a, b); + } + say("a branch a line cyc/branch"); + for (k = 0; k < sizeof(C) / sizeof(C[0]); k++) + { + long long c = cpi_from(C[k].fn, NULL, 1600000ul / C[k].n) - e_d_d; + fmt(a, (int)(c / C[k].n)); + say(" %3d lines %s", C[k].n, a); + } + /* The data side: the pointer chase round a few lines, all cached. */ + { + static const unsigned char lines[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 14, 16, 24, 32 }; + size_t bufsize; + unsigned char *big = rb->plugin_get_buffer(&bufsize); + big = (unsigned char *)(((unsigned long)big + 31) & ~31ul); + say("chase, cached cyc/load"); + for (k = 0; k < sizeof(lines); k++) + { + make_chain(big, lines[k] * 32, 32); + fmt(a, per_insn(cpi(chase, (unsigned long *)big), e_d_d)); + say(" %3d lines %s", lines[k], a); + } + } +} + +static void say_rows(const struct row *r, int n) +{ + int k; + for (k = 0; k < n; k++) + { + char d[8]; + int div = r[k].per, sub = r[k].alu * 100; + fmt(d, (per_insn(cpi(r[k].d, dram_buf), e_d_d) - sub) / div); +#ifdef USE_IRAM + { + char a[8], b[8], c[8]; + fmt(a, (per_insn(cpi(r[k].i, iram_buf), e_i_i) - sub) / div); + fmt(b, (per_insn(cpi(r[k].i, dram_buf), e_i_d) - sub) / div); + fmt(c, (per_insn(cpi(r[k].d, iram_buf), e_d_i) - sub) / div); + say("%s %s %s %s %s", r[k].name, a, b, c, d); + } +#else + say("%s %s", r[k].name, d); +#endif + } +} + +/* Define to run only the newest rows, for a quick turn on a device + whose other rows are already known. */ +/* #define TEST_CYC_QUICK */ + enum plugin_status plugin_start(const void* parameter) { - long long e_d_d, loop_cyc; -#ifdef USE_IRAM - long long e_i_i, e_i_d, e_d_i; -#endif - int k; + long long loop_cyc; (void)parameter; rb->lcd_setfont(FONT_SYSFIXED); @@ -438,23 +1991,75 @@ enum plugin_status plugin_start(const void* parameter) say("cyc/insn"); #endif - for (k = 0; k < NB; k++) +#if ARM_ARCH >= 5 && !defined(CPU_ARM_MICRO) { - char d[8]; - int div = B[k].per, sub = B[k].alu * 100; - fmt(d, (per_insn(cpi(B[k].d, dram_buf), e_d_d) - sub) / div); -#ifdef USE_IRAM - { - char a[8], b[8], c[8]; - fmt(a, (per_insn(cpi(B[k].i, iram_buf), e_i_i) - sub) / div); - fmt(b, (per_insn(cpi(B[k].i, dram_buf), e_i_d) - sub) / div); - fmt(c, (per_insn(cpi(B[k].d, iram_buf), e_d_i) - sub) / div); - say("%s %s %s %s %s", B[k].name, a, b, c, d); - } -#else - say("%s %s", B[k].name, d); -#endif + /* The control register, whose bit 14 picks round-robin cache + replacement over random, and the cache type register. */ + unsigned long ctrl, ctype; + asm volatile ("mrc p15, 0, %0, c1, c0, 0" : "=r" (ctrl)); + asm volatile ("mrc p15, 0, %0, c0, c0, 1" : "=r" (ctype)); + say("cp15 control %08lx (RR bit %lu), cache type %08lx", ctrl, + (ctrl >> 14) & 1, ctype); } +#endif + +#ifdef TEST_CYC_QUICK + say("quick run: the newest rows only"); +#if ARM_ARCH >= 5 + say_window_rows_everywhere(); +#else + say_miss_rows_everywhere(); +#endif +#else + say_rows(B, NB); + say_rows(B2, NB2); + say_rows(B3, NB3); + say_filter_rows(); +#if TEST_CYC_ARCHIVE + say_filter_layout(); +#endif + say_code_size_rows(); +#ifndef TICK_TIMED +#if TEST_CYC_ARCHIVE + say_evict_rows(); + say_evict_set_rows(); +#endif + say_evict_lag_rows(); +#endif + say_miss_rows_everywhere(); +#if ARM_ARCH >= 5 + say_code_miss_rows_everywhere(); + say_window_rows_everywhere(); +#endif + + /* Stack traffic, here and on the codec thread. */ +#ifdef USE_IRAM + sp_empty_i = e_i_d; +#endif + sp_empty_d = e_d_d; + stack_rows(); + say_stack_rows("main"); + sp_done = false; + rb->codec_thread_do_callback(stack_rows, NULL); + while (!sp_done) + rb->sleep(HZ / 10); + rb->codec_thread_do_callback(NULL, NULL); + say_stack_rows("codec"); + +#ifdef IRAM_MAP_BYTES + /* What a store costs from IRAM code, by where in IRAM it lands. */ + { + unsigned long off; + say("iram store map, code at %08lx", (unsigned long)b_str_i); + for (off = 0; off < IRAM_MAP_BYTES; off += 4096) + { + char a[8]; + unsigned long *q = &iram_map[off / 4]; + fmt(a, per_insn(cpi(b_str_i, q), e_i_i)); + say(" %08lx %s", (unsigned long)q, a); + } + } +#endif /* Misses: a pointer chase over working sets either side of the cache, in DRAM code, from the plugin buffer. The loop's own cost is the @@ -514,8 +2119,168 @@ enum plugin_status plugin_start(const void* parameter) say(" %s %s %s %s", S[k2].name, a, b, c); } } + + /* Stores: to 1 KB that was read first, so its lines are cached; + to 1 KB after 32 KB elsewhere was read, so they are not, unless + a store brings its line in; and over the large buffer. */ + { + static const struct { const char *name; benchfn fn; } W[] = { + { "word ", wstream_word }, + { "line 32", wstream_line }, + { "stm 4 ", wstream_stm4 }, + }; + unsigned long large = bufsize > 262144 ? 262144 : bufsize; + large &= ~511ul; + say("store stream 1K read 1K cold %luKB", large / 1024); + for (k2 = 0; k2 < sizeof(W) / sizeof(W[0]); k2++) + { + char a[8], b[8], c[8]; + sbuf = (unsigned long *)big; + sbytes = 1024; + stream_word(NULL, 64); + fmt(a, per_insn(cpi(W[k2].fn, NULL), e_d_d)); + if (bufsize >= 131072) + { + sbuf = (unsigned long *)(big + 65536); + sbytes = 32768; + stream_line(NULL, 256); + } + sbuf = (unsigned long *)big; + sbytes = 1024; + fmt(b, per_insn(cpi_from(W[k2].fn, NULL, ITERS), e_d_d)); + sbytes = large; + fmt(c, per_insn(cpi(W[k2].fn, NULL), e_d_d)); + say(" %s %s %s %s", W[k2].name, a, b, c); + } + } + + /* A miss with work after it. */ + { + static const struct { const char *name; benchfn fn; } K[] = { + { "+0 alu ", stream_line }, + { "+8 alu ", stream_work8 }, + { "+16 alu", stream_work16 }, + { "+32 alu", stream_work32 }, + }; + unsigned long large = bufsize > 262144 ? 262144 : bufsize; + large &= ~511ul; + say("load a line cached %luKB extra", large / 1024); + for (k2 = 0; k2 < sizeof(K) / sizeof(K[0]); k2++) + { + char a[8], b[8], c[8]; + int hit, miss; + sbuf = (unsigned long *)big; + sbytes = 1024; + hit = per_insn(cpi(K[k2].fn, NULL), e_d_d); + sbytes = large; + miss = per_insn(cpi(K[k2].fn, NULL), e_d_d); + fmt(a, hit); + fmt(b, miss); + fmt(c, miss - hit); + say(" %s %s %s %s", K[k2].name, a, b, c); + } + } + + /* Which word of the line the miss asks for, and what it costs + when the lines it evicts are dirty. */ + { + static const struct { const char *name; benchfn fn; } O[] = { + { "word 0 ", stream_first }, + { "word 3 ", stream_mid }, + { "word 7 ", stream_last }, +#if TEST_CYC_ARCHIVE + { "word 3+rest", stream_mid_rest }, + { "word 7+rest", stream_last_rest }, + { "w0, dirty 4", stream_dirty4 }, + { "w0, dirty 8", stream_dirty8 }, + { "dirty 8 +16", stream_dirty8_w16 }, +#endif + { "dirty 8 +32", stream_dirty8_w32 }, + { "dirty 8 +64", stream_dirty8_w64 }, + { "dirty 8 +96", stream_dirty8_w96 }, +#if TEST_CYC_ARCHIVE + { "word 7 +16 ", stream_last_w16 }, +#endif + { "word 7 +32 ", stream_last_w32 }, + }; + unsigned long large = bufsize > 262144 ? 262144 : bufsize; + large &= ~511ul; + say("miss on cached %luKB extra", large / 1024); + for (k2 = 0; k2 < sizeof(O) / sizeof(O[0]); k2++) + { + char a[8], b[8], c[8]; + int hit, miss; + sbuf = (unsigned long *)big; + sbytes = 1024; + hit = per_insn(cpi(O[k2].fn, NULL), e_d_d); + sbytes = large; + miss = per_insn(cpi(O[k2].fn, NULL), e_d_d); + fmt(a, hit); + fmt(b, miss); + fmt(c, miss - hit); + say(" %s %s %s %s", O[k2].name, a, b, c); + } + } + +#if TEST_CYC_ARCHIVE + /* Two buffers filtered in turn and then rewritten together: cycles + a pass of the rewrite, by buffer size and how far apart they + are. 512 bytes each stay cached; 16 KB each do not both fit. */ + if (bufsize >= 131072) + { + static const struct { unsigned bytes, apart; } P[] = { + { 512, 32768 }, { 4096, 32768 }, { 8192, 32768 }, + { 16384, 32768 }, { 16384, 34816 }, { 16384, 16384 }, + { 32768, 32768 }, + }; + say("pair rewrite cyc/pass, 2 x 16 bytes"); + for (k2 = 0; k2 < sizeof(P) / sizeof(P[0]); k2++) + { + char a[8]; + long long both, filt; + pair_a = (unsigned long *)big; + pair_b = (unsigned long *)(big + P[k2].apart); + pair_passes = P[k2].bytes / 16; + filt = cpi_from(pair_filters, NULL, 4); + both = cpi_from(pair_frame, NULL, 4); + fmt(a, (int)((both - filt) / pair_passes)); + say(" %2uK, %2uK apart %s", P[k2].bytes / 1024, + P[k2].apart / 1024, a); + } + } +#endif + + /* Loads far apart: the chase again, in scattered order. */ + { + static const unsigned long sizes[] = { 65536, 262144, 1048576 }; + say("chase scattered stride 32"); + for (k2 = 0; k2 < sizeof(sizes) / sizeof(sizes[0]); k2++) + { + char a[8]; + if (sizes[k2] > bufsize) + break; + make_chain_scattered(big, sizes[k2], 32); + fmt(a, per_insn(cpi(chase, (unsigned long *)big), e_d_d)); + say(" %4lu KB %s", sizes[k2] / 1024, a); + } + } } + /* Instruction fetch misses: a taken branch per line of DRAM code, over + 32 lines of it and over 2048. */ + { + char a[8], b[8], c[8]; + int small = (int)((cpi_from(ichain_small, NULL, 4096) - e_d_d) + / ICHAIN_SMALL); + int big2 = (int)((cpi_from(ichain_big, NULL, 64) - e_d_d) + / ICHAIN_BIG); + fmt(a, small); + fmt(b, big2); + fmt(c, big2 - small); + say("code chain cyc/branch small %s big %s extra %s", a, b, c); + } +#endif /* TEST_CYC_QUICK */ + #ifdef HAVE_ADJUSTABLE_CPU_FREQ rb->cpu_boost(false); #endif From 619f2f1093c7a1a05743d87f6ffb5f218e2d790b Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Tue, 6 Oct 2026 23:38:25 -0400 Subject: [PATCH 402/408] afr: fix gapless playback with fatigue reduction enabled afr_configure() returned 1 for every message. For DSP_PROC_NEW_FORMAT that is PROC_NEW_FORMAT_DEACTIVATED, so the DSP did not call the stage for the buffer that brought a new format: the first buffer of each track went out unfiltered, and the filters started on the second. That is 128 samples of a WAV file, and the whole first block of a codec with 32-bit non-interleaved output, 4608 samples of a FLAC file. A codec sets the format again at the start of every track, so between gapless tracks this put a short stretch of unfiltered audio into filtered audio, heard as a click at the join. Return 0, as the other stages do. Found with test_codec's DSP checksums on a Sansa Clip+: a model of the DSP with fatigue reduction left out of each file's first buffer reproduces the sixteen checksums of two runs, and none of them with it left in. Listening on the same player, the click between gapless tracks is gone with this change. Change-Id: I7a1193fa0a4ee0c983cbcc5c1af201e1678579d0 Co-Authored-By: Claude Opus 5.5 --- lib/rbcodec/dsp/afr.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rbcodec/dsp/afr.c b/lib/rbcodec/dsp/afr.c index 0ec2b2cf4e..dcdafba591 100644 --- a/lib/rbcodec/dsp/afr.c +++ b/lib/rbcodec/dsp/afr.c @@ -137,7 +137,7 @@ static intptr_t afr_configure(struct dsp_proc_entry *this, break; } - return 1; + return 0; } /* Database entry */ From 27a8ca3f55ebb1110da0390d6f7cbd382f1b2766 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Thu, 8 Oct 2026 22:23:21 -0400 Subject: [PATCH 403/408] test_codec: stop SID files after 2 minutes of decoding The SID codec never finishes on its own, so test_codec would decode forever and a directory test would stall on the first SID file. Count the decoded samples and halt the codec after 120 seconds of audio. Also set the track length to that limit so the benchmark results are correct and show the elapsed time while decoding. Fixes FS#13662 Co-Authored-By: Claude Opus 5.5 Change-Id: I4e7e99bf41d00c5597aae9d48b837e28df5b75c0 --- apps/plugins/test_codec.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apps/plugins/test_codec.c b/apps/plugins/test_codec.c index 9ba111f4bd..7e1ab3304f 100644 --- a/apps/plugins/test_codec.c +++ b/apps/plugins/test_codec.c @@ -146,6 +146,10 @@ static bool checksum; static uint32_t crc32; static volatile unsigned int elapsed; +/* Codecs such as SID never end on their own, so limit how long they run */ +#define SID_MAX_SECONDS 120 +static unsigned long codec_frequency; /* Codec output rate before DSP */ +static unsigned long decoded_samples; /* Samples output by the codec */ static volatile bool codec_playing; static volatile long codec_action; static volatile long endtick; @@ -274,6 +278,8 @@ static int process_dsp(const void *ch1, const void *ch2, int count) /* Null output */ static void pcmbuf_insert_null(const void *ch1, const void *ch2, int count) { + decoded_samples += count; + if (use_dsp) process_dsp(ch1, ch2, count); @@ -323,6 +329,8 @@ static int fill_buffer(int new_offset){ /* WAV output or calculate crc32 of output*/ static void pcmbuf_insert_wav_checksum(const void *ch1, const void *ch2, int count) { + decoded_samples += count; + /* Prevent idle poweroff */ rb->reset_poweroff_timer(); @@ -506,6 +514,15 @@ static void seek_complete(void) static long get_command(intptr_t *param) { rb->yield(); + + /* SID files play forever, so stop them after a fixed time */ + if (track.id3.codectype == AFMT_SID && codec_frequency >= 1000) + { + elapsed = decoded_samples / (codec_frequency / 1000); + if (decoded_samples >= SID_MAX_SECONDS * codec_frequency) + return CODEC_ACTION_HALT; + } + return codec_action; (void)param; } @@ -531,6 +548,7 @@ static void configure(int setting, intptr_t value) { case DSP_SET_FREQUENCY: DEBUGF("samplerate=%d\n",(int)value); + codec_frequency = value; if (use_dsp) { wavinfo.samplerate = rb->dsp_configure( ci.dsp, DSP_GET_OUT_FREQUENCY, 0); @@ -698,6 +716,11 @@ static enum plugin_status test_track(const char* filename) goto exit; } + /* SID length is the subsong count, use the decode limit instead so the + benchmark results are meaningful */ + if (track.id3.codectype == AFMT_SID) + track.id3.length = SID_MAX_SECONDS * 1000; + if (track.filesize > audiosize) { audiobufsize=audiosize; @@ -742,6 +765,8 @@ static enum plugin_status test_track(const char* filename) codec_playing = true; codec_action = CODEC_ACTION_NULL; + codec_frequency = 0; + decoded_samples = 0; rb->codec_thread_do_callback(codec_thread, NULL); From 5880b6111c3058aeb0eac1e37eae17c4281e8c21 Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Thu, 8 Oct 2026 23:01:48 -0400 Subject: [PATCH 404/408] metadata: read R128 gain tags in Opus files RFC 7845 specifies R128_TRACK_GAIN and R128_ALBUM_GAIN for loudness normalization in Opus files, but Rockbox only understood the REPLAYGAIN_* tags. Parse the R128 tags as Q7.8 dB values relative to -23 LUFS and add 5 dB to match the ReplayGain reference level. If a file has both kinds of tags, the R128 tags take priority. Fixes FS#13624 Co-Authored-By: Claude Opus 5.5 Change-Id: I0ef08a7897ec5ee8d1e5d99e7d9ea3901847fa52 --- lib/rbcodec/metadata/replaygain.c | 46 +++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/lib/rbcodec/metadata/replaygain.c b/lib/rbcodec/metadata/replaygain.c index 3283a7fd67..864a2c74ea 100644 --- a/lib/rbcodec/metadata/replaygain.c +++ b/lib/rbcodec/metadata/replaygain.c @@ -138,6 +138,26 @@ static long get_replaygain(const char* str) return fp_atof(str, FP_BITS); } +/* Get the gain in Q19.12 format from an Opus R128 gain tag (RFC 7845). + * Returns false if the tag is not a valid integer. + * + * str Gain in dB as a Q7.8 integer, relative to -23 LUFS. + */ +static bool get_r128_gain(const char* str, long* gain) +{ + char* end; + long q8 = strtol(str, &end, 10); + + if (end == str || q8 < -32768 || q8 > 32767) + { + return false; + } + + /* ReplayGain targets -18 LUFS, so add 5 dB to match its loudness */ + *gain = q8 * (FP_ONE / 256) + 5 * FP_ONE; + return true; +} + /* Get the peak volume in Q7.24 format. * * str Peak volume. Full scale is specified as "1.0". Returns 0 for no peak. @@ -158,7 +178,8 @@ long get_replaygain_int(long int_gain) /* Parse a ReplayGain tag conforming to the "VorbisGain standard". If a * valid tag is found, update mp3entry struct accordingly. Existing values - * are not overwritten. + * are not overwritten, except by the R128 gain tags in Opus files, which + * take priority over REPLAYGAIN tags. * * key Name of the tag. * value Value of the tag. @@ -170,9 +191,30 @@ void parse_replaygain(const char* key, const char* value, static const char *rg_options[] = {"replaygain_track_gain", "rg_radio", "replaygain_album_gain", "rg_audiophile", "replaygain_track_peak", "rg_peak", - "replaygain_album_peak", NULL}; + "replaygain_album_peak", + "r128_track_gain", "r128_album_gain", + NULL}; int rg_op = string_option(key, rg_options, true); + long gain; + + if (rg_op == 7 || rg_op == 8) + { /*r128_track_gain||r128_album_gain, only defined for Opus*/ + if (entry->codectype == AFMT_OPUS && get_r128_gain(value, &gain)) + { + if (rg_op == 7) + { + entry->track_level = gain; + entry->track_gain = convert_gain(gain); + } + else + { + entry->album_level = gain; + entry->album_gain = convert_gain(gain); + } + } + return; + } if ((rg_op == 0 || rg_op == 1) && !entry->track_gain) { /*replaygain_track_gain||rg_radio*/ From b34fcd602a2813fe239ee728cd3242bd17cdeac6 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Thu, 8 Oct 2026 15:03:10 -0400 Subject: [PATCH 405/408] mikmod: Update to libmikmod 3.3.14 Fixes several loader issues Change-Id: Iaa5136bc6da85ba846d0dd38bfdb0ed610200147 --- apps/plugins/mikmod/README.rockbox | 4 +-- apps/plugins/mikmod/load_amf.c | 9 +++++- apps/plugins/mikmod/load_dsm.c | 8 ++++- apps/plugins/mikmod/load_imf.c | 30 ------------------- apps/plugins/mikmod/load_it.c | 41 ++------------------------ apps/plugins/mikmod/load_okt.c | 7 ++++- apps/plugins/mikmod/load_ult.c | 12 ++++++-- apps/plugins/mikmod/load_uni.c | 34 ++++----------------- apps/plugins/mikmod/load_xm.c | 23 --------------- apps/plugins/mikmod/mdreg.c | 6 ++++ apps/plugins/mikmod/mikmod.h | 7 +++-- apps/plugins/mikmod/mikmod_internals.h | 10 +++++-- apps/plugins/mikmod/mmerror.c | 4 +-- apps/plugins/mikmod/virtch_common.c | 22 -------------- 14 files changed, 62 insertions(+), 155 deletions(-) diff --git a/apps/plugins/mikmod/README.rockbox b/apps/plugins/mikmod/README.rockbox index 22f17b7b41..2e0cc923ab 100644 --- a/apps/plugins/mikmod/README.rockbox +++ b/apps/plugins/mikmod/README.rockbox @@ -1,7 +1,7 @@ mikmod (https://mikmod.sourceforge.net/) -Based on upstream commit 72424e1bebab470fbf8a2914dcd7903b742205e6 -corresponding to mikmod 3.2.9 / libmikmod 3.3.12 +Based on upstream commit 0c6c2f7f8fb8dd6954ab0407b400e9646b9c2cf0 +corresponding to pre mikmod 3.2.10 / libmikmod 3.3.14 Heavily customized for rockbox use: diff --git a/apps/plugins/mikmod/load_amf.c b/apps/plugins/mikmod/load_amf.c index 7d25430aac..aa7db9a5a9 100644 --- a/apps/plugins/mikmod/load_amf.c +++ b/apps/plugins/mikmod/load_amf.c @@ -424,8 +424,15 @@ static int AMF_Load(int curious) if(mh->version>=11) { memset(mh->panpos,0,32); _mm_read_SBYTES(mh->panpos,(mh->version>=13)?32:16,modreader); - } else if(mh->version>=9) + } else if(mh->version>=9) { _mm_read_UBYTES(channel_remap,16,modreader); + for(t = 0; t < 16 && t < mh->numchannels; t++) { + if(channel_remap[t] >= mh->numchannels) { + _mm_errno=MMERR_NOT_A_MODULE; + return 0; + } + } + } if (mh->version>=13) { mh->songbpm=_mm_read_UBYTE(modreader); diff --git a/apps/plugins/mikmod/load_dsm.c b/apps/plugins/mikmod/load_dsm.c index 97d54f429e..da8024f10d 100644 --- a/apps/plugins/mikmod/load_dsm.c +++ b/apps/plugins/mikmod/load_dsm.c @@ -238,6 +238,7 @@ static int DSM_Load(int curious) DSMINST s; SAMPLE *q; int cursmp=0,curpat=0,track=0; + unsigned int numtrk; (void)curious; blocklp=0; @@ -270,7 +271,12 @@ static int DSM_Load(int curious) of.modtype=MikMod_strdup(DSM_Version); of.numchn=mh->numtrk; of.numpat=mh->numpat; - of.numtrk=of.numchn*of.numpat; + numtrk=of.numchn*of.numpat; + if(of.numchn>DSM_MAXCHAN || numtrk>65535) { + _mm_errno = MMERR_LOADING_HEADER; + return 0; + } + of.numtrk=(UWORD)numtrk; of.songname=DupStr(mh->songname,28,1); /* make a cstr of songname */ of.reppos=0; of.flags |= UF_PANNING; diff --git a/apps/plugins/mikmod/load_imf.c b/apps/plugins/mikmod/load_imf.c index b67e3ac828..f580aac660 100644 --- a/apps/plugins/mikmod/load_imf.c +++ b/apps/plugins/mikmod/load_imf.c @@ -541,7 +541,6 @@ static int IMF_Load(int curious) _mm_read_I_UWORDS(ih.panenv,IMFENVCNT,modreader); _mm_read_I_UWORDS(ih.pitenv,IMFENVCNT,modreader); -#if defined __STDC__ || defined _MSC_VER || defined __WATCOMC__ || defined MPW_C #define IMF_FinishLoadingEnvelope(name) \ ih. name##pts=_mm_read_UBYTE(modreader); \ ih. name##sus=_mm_read_UBYTE(modreader); \ @@ -551,17 +550,6 @@ static int IMF_Load(int curious) _mm_skip_BYTE(modreader); \ _mm_skip_BYTE(modreader); \ _mm_skip_BYTE(modreader) -#else -#define IMF_FinishLoadingEnvelope(name) \ - ih. name/**/pts=_mm_read_UBYTE(modreader); \ - ih. name/**/sus=_mm_read_UBYTE(modreader); \ - ih. name/**/beg=_mm_read_UBYTE(modreader); \ - ih. name/**/end=_mm_read_UBYTE(modreader); \ - ih. name/**/flg=_mm_read_UBYTE(modreader); \ - _mm_skip_BYTE(modreader); \ - _mm_skip_BYTE(modreader); \ - _mm_skip_BYTE(modreader) -#endif IMF_FinishLoadingEnvelope(vol); IMF_FinishLoadingEnvelope(pan); @@ -594,7 +582,6 @@ static int IMF_Load(int curious) d->samplenumber[u]=ih.what[u]>ih.numsmp?0xffff:ih.what[u]+of.numsmp; d->volfade=ih.volfade; -#if defined __STDC__ || defined _MSC_VER || defined __WATCOMC__ || defined MPW_C #define IMF_ProcessEnvelope(name) \ for (u = 0; u < (IMFENVCNT >> 1); u++) { \ d-> name##env[u].pos = ih. name##env[u << 1]; \ @@ -610,23 +597,6 @@ static int IMF_Load(int curious) \ if ((d-> name##flg&EF_ON)&&(d-> name##pts<2)) \ d-> name##flg&=~EF_ON -#else -#define IMF_ProcessEnvelope(name) \ - for (u = 0; u < (IMFENVCNT >> 1); u++) { \ - d-> name/**/env[u].pos = ih. name/**/env[u << 1]; \ - d-> name/**/env[u].val = ih. name/**/env[(u << 1)+ 1]; \ - } \ - if (ih. name/**/flg&1) d-> name/**/flg|=EF_ON; \ - if (ih. name/**/flg&2) d-> name/**/flg|=EF_SUSTAIN; \ - if (ih. name/**/flg&4) d-> name/**/flg|=EF_LOOP; \ - d-> name/**/susbeg=d-> name/**/susend=ih. name/**/sus; \ - d-> name/**/beg=ih. name/**/beg; \ - d-> name/**/end=ih. name/**/end; \ - d-> name/**/pts=ih. name/**/pts; \ - \ - if ((d-> name/**/flg&EF_ON)&&(d-> name/**/pts<2)) \ - d-> name/**/flg&=~EF_ON -#endif IMF_ProcessEnvelope(vol); IMF_ProcessEnvelope(pan); diff --git a/apps/plugins/mikmod/load_it.c b/apps/plugins/mikmod/load_it.c index 4442c209c0..7fa2495db5 100644 --- a/apps/plugins/mikmod/load_it.c +++ b/apps/plugins/mikmod/load_it.c @@ -194,7 +194,7 @@ static int IT_Init(void) { if(!(mh=(ITHEADER*)MikMod_malloc(sizeof(ITHEADER)))) return 0; if(!(poslookup=(UBYTE*)MikMod_malloc(256*sizeof(UBYTE)))) return 0; - if(!(itpat=(ITNOTE*)MikMod_malloc(200*64*sizeof(ITNOTE)))) return 0; + if(!(itpat=(ITNOTE*)MikMod_malloc(256*64*sizeof(ITNOTE)))) return 0; if(!(mask=(UBYTE*)MikMod_calloc(64,sizeof(UBYTE)))) return 0; if(!(last=(ITNOTE*)MikMod_calloc(64,sizeof(ITNOTE)))) return 0; @@ -338,10 +338,10 @@ static int IT_ReadPattern(UWORD patrows) int row=0,flag,ch; unsigned int blah; ITNOTE *itt=itpat,dummy,*n,*l; - ITNOTE *ite=&itpat[200*64 -1]; + ITNOTE *ite=&itpat[256*64 -1]; UBYTE *m; - memset(itt,255,200*64*sizeof(ITNOTE)); + memset(itt,255,256*64*sizeof(ITNOTE)); do { if(_mm_eof(modreader)) { @@ -783,7 +783,6 @@ static int IT_Load(int curious) } } else { /* load IT 2xx volume, pan and pitch envelopes */ -#if defined __STDC__ || defined _MSC_VER || defined __WATCOMC__ || defined MPW_C #define IT_LoadEnvelope(name,type) \ ih. name##flg =_mm_read_UBYTE(modreader); \ ih. name##pts =_mm_read_UBYTE(modreader); \ @@ -798,22 +797,6 @@ static int IT_Load(int curious) ih. name##tick[lp]=_mm_read_I_UWORD(modreader); \ } \ _mm_skip_BYTE(modreader) -#else -#define IT_LoadEnvelope(name,type) \ - ih. name/**/flg =_mm_read_UBYTE(modreader); \ - ih. name/**/pts =_mm_read_UBYTE(modreader); \ - if (ih. name/**/pts > ITENVCNT) \ - ih. name/**/pts = ITENVCNT; \ - ih. name/**/beg =_mm_read_UBYTE(modreader); \ - ih. name/**/end =_mm_read_UBYTE(modreader); \ - ih. name/**/susbeg=_mm_read_UBYTE(modreader); \ - ih. name/**/susend=_mm_read_UBYTE(modreader); \ - for(lp=0;lprpanvar = ih.rpanvar; } -#if defined __STDC__ || defined _MSC_VER || defined __WATCOMC__ || defined MPW_C #define IT_ProcessEnvelope(name) \ if(ih. name##flg&1) d-> name##flg|=EF_ON; \ if(ih. name##flg&2) d-> name##flg|=EF_LOOP; \ @@ -891,23 +873,6 @@ static int IT_Load(int curious) \ if((d-> name##flg&EF_ON)&&(d-> name##pts<2)) \ d-> name##flg&=~EF_ON -#else -#define IT_ProcessEnvelope(name) \ - if(ih. name/**/flg&1) d-> name/**/flg|=EF_ON; \ - if(ih. name/**/flg&2) d-> name/**/flg|=EF_LOOP; \ - if(ih. name/**/flg&4) d-> name/**/flg|=EF_SUSTAIN; \ - d-> name/**/pts=ih. name/**/pts; \ - d-> name/**/beg=ih. name/**/beg; \ - d-> name/**/end=ih. name/**/end; \ - d-> name/**/susbeg=ih. name/**/susbeg; \ - d-> name/**/susend=ih. name/**/susend; \ - \ - for(u=0;u name/**/env[u].pos=ih. name/**/tick[u]; \ - \ - if((d-> name/**/flg&EF_ON)&&(d-> name/**/pts<2)) \ - d-> name/**/flg&=~EF_ON -#endif IT_ProcessEnvelope(vol); diff --git a/apps/plugins/mikmod/load_okt.c b/apps/plugins/mikmod/load_okt.c index af3b7c53de..516282e7b0 100644 --- a/apps/plugins/mikmod/load_okt.c +++ b/apps/plugins/mikmod/load_okt.c @@ -282,7 +282,12 @@ static int OKT_doPBOD(int patnum) int u; if (!patnum) { - of.numtrk = of.numpat * of.numchn; + const unsigned int numtrk = of.numpat * of.numchn; + if (numtrk > 65535) { + _mm_errno = MMERR_LOADING_PATTERN; + return 0; + } + of.numtrk = (UWORD)numtrk; if (!AllocTracks() || !AllocPatterns()) return 0; diff --git a/apps/plugins/mikmod/load_ult.c b/apps/plugins/mikmod/load_ult.c index 58c66d16aa..0c695659a7 100644 --- a/apps/plugins/mikmod/load_ult.c +++ b/apps/plugins/mikmod/load_ult.c @@ -125,6 +125,7 @@ static UBYTE ReadUltEvent(ULTEVENT* event) static int ULT_Load(int curious) { int t,u,tracks=0; + unsigned int numtrk; SAMPLE *q; ULTSAMPLE s; ULTHEADER mh; @@ -211,9 +212,14 @@ static int ULT_Load(int curious) noc=_mm_read_UBYTE(modreader); RBnop=_mm_read_UBYTE(modreader); - of.numchn=++noc; - of.numpat=++RBnop; - of.numtrk=of.numchn*of.numpat; + of.numchn=noc+1; + of.numpat=RBnop+1; + numtrk=of.numchn*of.numpat; + if (numtrk>65535) { + _mm_errno = MMERR_LOADING_HEADER; + return 0; + } + of.numtrk=(UWORD)numtrk; for(t=0;t<256;t++) { if(of.positions[t]==255) { diff --git a/apps/plugins/mikmod/load_uni.c b/apps/plugins/mikmod/load_uni.c index 3a911053f9..fdd689b183 100644 --- a/apps/plugins/mikmod/load_uni.c +++ b/apps/plugins/mikmod/load_uni.c @@ -307,7 +307,6 @@ static int loadinstr6(void) i->rpanvar = _mm_read_UBYTE(modreader); i->volfade = _mm_read_M_UWORD(modreader); -#if defined __STDC__ || defined _MSC_VER || defined __WATCOMC__ || defined MPW_C #define UNI_LoadEnvelope6(name) \ i-> name##flg=_mm_read_UBYTE(modreader); \ i-> name##pts=_mm_read_UBYTE(modreader); \ @@ -315,23 +314,11 @@ static int loadinstr6(void) i-> name##susend=_mm_read_UBYTE(modreader); \ i-> name##beg=_mm_read_UBYTE(modreader); \ i-> name##end=_mm_read_UBYTE(modreader); \ + if (i-> name##pts > ENVPOINTS) goto fail; \ for(w=0;w<(universion>=0x100?32:i-> name##pts);w++) { \ i-> name##env[w].pos=_mm_read_M_SWORD(modreader); \ i-> name##env[w].val=_mm_read_M_SWORD(modreader); \ } -#else -#define UNI_LoadEnvelope6(name) \ - i-> name/**/flg=_mm_read_UBYTE(modreader); \ - i-> name/**/pts=_mm_read_UBYTE(modreader); \ - i-> name/**/susbeg=_mm_read_UBYTE(modreader); \ - i-> name/**/susend=_mm_read_UBYTE(modreader); \ - i-> name/**/beg=_mm_read_UBYTE(modreader); \ - i-> name/**/end=_mm_read_UBYTE(modreader); \ - for (w=0;w<(universion>=0x100?32:i-> name/**/pts);w++) { \ - i-> name/**/env[w].pos=_mm_read_M_SWORD(modreader); \ - i-> name/**/env[w].val=_mm_read_M_SWORD(modreader); \ - } -#endif UNI_LoadEnvelope6(vol); UNI_LoadEnvelope6(pan); @@ -348,6 +335,7 @@ static int loadinstr6(void) i->insname=readstring(); if(_mm_eof(modreader)) { + fail: _mm_errno = MMERR_LOADING_SAMPLEINFO; return 0; } @@ -372,7 +360,6 @@ static int loadinstr5(void) for(u=0;u<96;u++) i->samplenumber[u]=of.numsmp+_mm_read_UBYTE(modreader); -#if defined __STDC__ || defined _MSC_VER || defined __WATCOMC__ || defined MPW_C #define UNI_LoadEnvelope5(name) \ i-> name##flg=_mm_read_UBYTE(modreader); \ i-> name##pts=_mm_read_UBYTE(modreader); \ @@ -384,19 +371,6 @@ static int loadinstr5(void) i-> name##env[u].pos=_mm_read_I_SWORD(modreader); \ i-> name##env[u].val=_mm_read_I_SWORD(modreader); \ } -#else -#define UNI_LoadEnvelope5(name) \ - i-> name/**/flg=_mm_read_UBYTE(modreader); \ - i-> name/**/pts=_mm_read_UBYTE(modreader); \ - i-> name/**/susbeg=_mm_read_UBYTE(modreader); \ - i-> name/**/susend=i-> name/**/susbeg; \ - i-> name/**/beg=_mm_read_UBYTE(modreader); \ - i-> name/**/end=_mm_read_UBYTE(modreader); \ - for(u=0;u<12;u++) { \ - i-> name/**/env[u].pos=_mm_read_I_SWORD(modreader); \ - i-> name/**/env[u].val=_mm_read_I_SWORD(modreader); \ - } -#endif UNI_LoadEnvelope5(vol); UNI_LoadEnvelope5(pan); @@ -619,6 +593,10 @@ static int UNI_Load(int curious) /* positions */ if(!AllocPositions(of.numpos)) return 0; if(universion>=6) { + if(mh.numchn>UF_MAXCHAN) { + _mm_errno=MMERR_LOADING_HEADER; + return 0; + } if(universion>=0x100) _mm_read_M_UWORDS(of.positions,of.numpos,modreader); else diff --git a/apps/plugins/mikmod/load_xm.c b/apps/plugins/mikmod/load_xm.c index 3dedfffbe5..13208d8698 100644 --- a/apps/plugins/mikmod/load_xm.c +++ b/apps/plugins/mikmod/load_xm.c @@ -533,7 +533,6 @@ static int LoadInstruments(void) d->samplenumber[u]=pth.what[u]+of.numsmp; d->volfade = pth.volfade; -#if defined __STDC__ || defined _MSC_VER || defined __WATCOMC__ || defined MPW_C #define XM_ProcessEnvelope(name) \ for (u = 0; u < XMENVPTS; u++) { \ d-> name##env[u].pos = pth. name##env[u << 1]; \ @@ -553,28 +552,6 @@ static int LoadInstruments(void) \ if ((d-> name##flg&EF_ON)&&(d-> name##pts<2)) \ d-> name##flg&=~EF_ON -#else -#define XM_ProcessEnvelope(name) \ - for (u = 0; u < XMENVPTS; u++) { \ - d-> name/**/env[u].pos = pth. name/**/env[u << 1]; \ - d-> name/**/env[u].val = pth. name/**/env[(u << 1)+ 1]; \ - } \ - if (pth. name/**/flg&1) d-> name/**/flg|=EF_ON; \ - if (pth. name/**/flg&2) d-> name/**/flg|=EF_SUSTAIN; \ - if (pth. name/**/flg&4) d-> name/**/flg|=EF_LOOP; \ - d-> name/**/susbeg=d-> name/**/susend= \ - pth. name/**/sus; \ - d-> name/**/beg=pth. name/**/beg; \ - d-> name/**/end=pth. name/**/end; \ - d-> name/**/pts=pth. name/**/pts; \ - \ - /* scale envelope */ \ - for (p = 0; p < XMENVPTS; p++) \ - d-> name/**/env[p].val<<=2; \ - \ - if ((d-> name/**/flg&EF_ON)&&(d-> name/**/pts<2)) \ - d-> name/**/flg&=~EF_ON -#endif XM_ProcessEnvelope(vol); XM_ProcessEnvelope(pan); diff --git a/apps/plugins/mikmod/mdreg.c b/apps/plugins/mikmod/mdreg.c index ff5d803000..b0ec4cf2fd 100644 --- a/apps/plugins/mikmod/mdreg.c +++ b/apps/plugins/mikmod/mdreg.c @@ -93,6 +93,9 @@ static void _mm_registeralldrivers(void) #ifdef DRV_OS2 _mm_registerdriver(&drv_os2); #endif +#ifdef DRV_WASAPI + _mm_registerdriver(&drv_wasapi); +#endif #ifdef DRV_XAUDIO2 _mm_registerdriver(&drv_xaudio2); #endif @@ -123,6 +126,9 @@ static void _mm_registeralldrivers(void) #ifdef DRV_N64 _mm_registerdriver(&drv_n64); #endif +#ifdef DRV_VITA + _mm_registerdriver(&drv_vita); +#endif /* dos drivers - wss first, since some cards emulate sb */ #ifdef DRV_WSS diff --git a/apps/plugins/mikmod/mikmod.h b/apps/plugins/mikmod/mikmod.h index 749b535bdc..343ca5073b 100644 --- a/apps/plugins/mikmod/mikmod.h +++ b/apps/plugins/mikmod/mikmod.h @@ -145,13 +145,13 @@ typedef unsigned short int UWORD; #if defined(_LP64) || defined(__LP64__) || defined(__arch64__) || defined(__alpha) || defined(__x86_64) || defined(__powerpc64__) /* 64 bit architectures: */ typedef signed int SLONG; -#if !defined(_MIKMOD_AMIGA) && !defined(WIN32) +#if !(defined(_WIN32) || defined(_MIKMOD_AMIGA)) typedef unsigned int ULONG; #endif #else /* 32 bit architectures: */ typedef signed long int SLONG; -#if !(defined(_MIKMOD_OS2) || defined(_MIKMOD_AMIGA)) +#if !(defined(_MIKMOD_OS2) || defined(_MIKMOD_WIN32) || defined(_MIKMOD_AMIGA)) typedef unsigned long int ULONG; #endif #endif @@ -293,6 +293,7 @@ enum { MMERR_MAX }; +#define MMERR_WASAPI_SAMPLERATE MMERR_SGI_SPEED /* * ========== Error handling @@ -826,6 +827,7 @@ MIKMODAPI extern struct MDRIVER drv_dart; /* OS/2 Direct Audio RealTime */ MIKMODAPI extern struct MDRIVER drv_os2; /* OS/2 MMPM/2 */ MIKMODAPI extern struct MDRIVER drv_ds; /* Win32 DirectSound driver */ +MIKMODAPI extern struct MDRIVER drv_wasapi; /* Win32 WASAPI driver */ MIKMODAPI extern struct MDRIVER drv_xaudio2;/* Win32 XAudio2 driver */ MIKMODAPI extern struct MDRIVER drv_win; /* Win32 multimedia API driver */ @@ -836,6 +838,7 @@ MIKMODAPI extern struct MDRIVER drv_dc; /* Dreamcast driver */ MIKMODAPI extern struct MDRIVER drv_gp32; /* GP32 Sound driver */ MIKMODAPI extern struct MDRIVER drv_psp; /* PlayStation Portable driver */ MIKMODAPI extern struct MDRIVER drv_n64; /* Nintendo64 driver */ +MIKMODAPI extern struct MDRIVER drv_vita; /* PlayStation Vita driver */ MIKMODAPI extern struct MDRIVER drv_wss; /* DOS WSS driver */ MIKMODAPI extern struct MDRIVER drv_sb; /* DOS S/B driver */ diff --git a/apps/plugins/mikmod/mikmod_internals.h b/apps/plugins/mikmod/mikmod_internals.h index f7ca8a1741..0f1d9613a4 100644 --- a/apps/plugins/mikmod/mikmod_internals.h +++ b/apps/plugins/mikmod/mikmod_internals.h @@ -33,6 +33,14 @@ extern "C" { #include +#if defined(_MSC_VER) +#pragma warning(disable:4389) /* signed/unsigned mismatch ( <, <=, >, >= ) */ +#pragma warning(disable:4018) /* signed/unsigned mismatch ( ==, != ) */ +#pragma warning(disable:4761) /* integral size mismatch in argument; conversion supplied (for MSVC6 and older.) */ +#pragma warning(disable:4244) /* conversion from 'type' to 'int', possible loss of data */ +#pragma warning(disable:4267) /* conversion from 'size_t' to 'type', possible loss of data */ +#endif + #if defined(_MSC_VER) && !defined(__cplusplus) && !defined(HAVE_CONFIG_H) #define inline __inline #endif @@ -902,5 +910,3 @@ void MikMod_afree(void *); /* frees if ptr != NULL */ #endif #endif /* _MIKMOD_INTERNALS_H */ - -/* ex:set ts=4: */ diff --git a/apps/plugins/mikmod/mmerror.c b/apps/plugins/mikmod/mmerror.c index ee7340ee27..99c4575022 100644 --- a/apps/plugins/mikmod/mmerror.c +++ b/apps/plugins/mikmod/mmerror.c @@ -132,14 +132,14 @@ static const char *_mm_errmsg[MMERR_MAX+1] = #endif /* SGI driver errors */ -#ifdef DRV_SGI "Unsupported sample rate", +#ifdef DRV_SGI "Hardware does not support 16 bit sound", "Hardware does not support 8 bit sound", "Hardware does not support stereo sound", "Hardware does not support mono sound", #else - _mmerr_invalid, _mmerr_invalid, _mmerr_invalid, + _mmerr_invalid, _mmerr_invalid, _mmerr_invalid, _mmerr_invalid, #endif diff --git a/apps/plugins/mikmod/virtch_common.c b/apps/plugins/mikmod/virtch_common.c index 70cd9417b2..dd665a1504 100644 --- a/apps/plugins/mikmod/virtch_common.c +++ b/apps/plugins/mikmod/virtch_common.c @@ -107,7 +107,6 @@ static int (*VC_VoiceStopped_ptr)(UBYTE); static SLONG (*VC_VoiceGetPosition_ptr)(UBYTE); static ULONG (*VC_VoiceRealVolume_ptr)(UBYTE); -#if defined __STDC__ || defined _MSC_VER || defined __WATCOMC__ || defined MPW_C #define VC_PROC0(suffix) \ MIKMODAPI void VC_##suffix (void) { VC_##suffix##_ptr(); } @@ -126,27 +125,6 @@ MIKMODAPI void VC_##suffix (typ1 a,typ2 b) { VC_##suffix##_ptr(a,b); } #define VC_FUNC2(suffix,ret,typ1,typ2) \ MIKMODAPI ret VC_##suffix (typ1 a,typ2 b) { return VC_##suffix##_ptr(a,b); } -#else - -#define VC_PROC0(suffix) \ -MIKMODAPI void VC_/**/suffix (void) { VC_/**/suffix/**/_ptr(); } - -#define VC_FUNC0(suffix,ret) \ -MIKMODAPI ret VC_/**/suffix (void) { return VC_/**/suffix/**/_ptr(); } - -#define VC_PROC1(suffix,typ1) \ -MIKMODAPI void VC_/**/suffix (typ1 a) { VC_/**/suffix/**/_ptr(a); } - -#define VC_FUNC1(suffix,ret,typ1) \ -MIKMODAPI ret VC_/**/suffix (typ1 a) { return VC_/**/suffix/**/_ptr(a); } - -#define VC_PROC2(suffix,typ1,typ2) \ -MIKMODAPI void VC_/**/suffix (typ1 a,typ2 b) { VC_/**/suffix/**/_ptr(a,b); } - -#define VC_FUNC2(suffix,ret,typ1,typ2) \ -MIKMODAPI ret VC_/**/suffix (typ1 a,typ2 b) { return VC_/**/suffix/**/_ptr(a,b); } -#endif - VC_FUNC0(Init,int) VC_PROC0(Exit) VC_FUNC0(SetNumVoices,int) From 8194d51c49a0ee5b3d7b7f3e1aef32580daf709f Mon Sep 17 00:00:00 2001 From: Michael Giacomelli Date: Fri, 9 Oct 2026 11:05:42 -0400 Subject: [PATCH 406/408] a52: play files that do not start with a frame A52 files cut from a longer stream can start partway through a frame. The metadata parser required a sync word at offset 0 and rejected them, although the codec can resync. Scan the first 64 KB for a frame header that is followed by a second valid header, record it as first_frame_offset, and use that offset when seeking and resuming. Tested in the simulator (perfsim) and on a Sansa Clip+: a file starting with 675 bytes of a partial frame now plays. In the simulator it decodes all 3161 frames with the same output as the file without those bytes, and seeks land on frame starts. Fixes FS#9056 Co-Authored-By: Claude Opus 5.5 Change-Id: I5d72b1cd1d8437491ec00a38459e391fb158b9a2 --- lib/rbcodec/codecs/a52.c | 12 +++-- lib/rbcodec/metadata/a52.c | 106 ++++++++++++++++++++++++++++++------- 2 files changed, 93 insertions(+), 25 deletions(-) diff --git a/lib/rbcodec/codecs/a52.c b/lib/rbcodec/codecs/a52.c index 7470148efe..b6e6c17b81 100644 --- a/lib/rbcodec/codecs/a52.c +++ b/lib/rbcodec/codecs/a52.c @@ -158,14 +158,15 @@ enum codec_status codec_run(void) samplesdone = 0; - if (ci->id3->offset) { - sample_loc = (ci->id3->offset / ci->id3->bytesperframe) * - A52_SAMPLESPERFRAME; + if (ci->id3->offset > ci->id3->first_frame_offset) { + sample_loc = ((ci->id3->offset - ci->id3->first_frame_offset) / + ci->id3->bytesperframe) * A52_SAMPLESPERFRAME; param = ci->id3->offset; } else if (ci->id3->elapsed) { sample_loc = ci->id3->elapsed/1000 * ci->id3->frequency; - param = sample_loc/A52_SAMPLESPERFRAME*ci->id3->bytesperframe; + param = sample_loc/A52_SAMPLESPERFRAME*ci->id3->bytesperframe + + ci->id3->first_frame_offset; } else { sample_loc = 0; @@ -190,7 +191,8 @@ enum codec_status codec_run(void) sample_loc = param/1000 * ci->id3->frequency; if (ci->seek_buffer((sample_loc/A52_SAMPLESPERFRAME)* - ci->id3->bytesperframe)) { + ci->id3->bytesperframe + + ci->id3->first_frame_offset)) { samplesdone = sample_loc; ci->set_elapsed(samplesdone/(ci->id3->frequency/1000)); } diff --git a/lib/rbcodec/metadata/a52.c b/lib/rbcodec/metadata/a52.c index 9f00b8d9f3..ab484d7f2b 100644 --- a/lib/rbcodec/metadata/a52.c +++ b/lib/rbcodec/metadata/a52.c @@ -19,6 +19,7 @@ ****************************************************************************/ #include +#include #include "metadata.h" #include "logf.h" #include "metadata_parsers.h" @@ -42,37 +43,107 @@ static const unsigned short a52_441framesizes[] = 1254 * 2, 1393 * 2, 1394 * 2 }; +/* How far into the file to look for the first frame */ +#define A52_MAX_SCAN (64 * 1024) +#define A52_HEADER_SIZE 6 + +/* Return the size in bytes of the frame starting with hdr, or 0 if hdr is + not a valid frame header. */ +static int a52_frame_size(const unsigned char *hdr) +{ + int frmsizecod = hdr[4] & 0x3f; + int bitrate; + + if ((hdr[0] != 0x0b) || (hdr[1] != 0x77) || (frmsizecod > 37) + || (hdr[5] >= 0x60)) /* bsid >= 12 is not A52 */ + { + return 0; + } + + bitrate = a52_bitrates[frmsizecod >> 1]; + + switch (hdr[4] & 0xc0) + { + case 0x00: + return bitrate * 2 * 2; + case 0x40: + return a52_441framesizes[frmsizecod]; + case 0x80: + return bitrate * 3 * 2; + default: + return 0; + } +} + +/* Find the first frame in the file, which is not at the start if the file + was cut from a longer stream. A match must be followed by a second valid + header so that sync words inside the audio data are skipped. Returns the + offset of the frame and leaves its header in hdr, or -1 if none found. */ +static off_t a52_find_first_frame(int fd, unsigned char *buf, size_t bufsize, + unsigned char *hdr) +{ + unsigned char next[A52_HEADER_SIZE]; + off_t pos = 0; + ssize_t n; + int i, size; + + while (pos < A52_MAX_SCAN) + { + if ((lseek(fd, pos, SEEK_SET) < 0) + || ((n = read(fd, buf, bufsize)) < A52_HEADER_SIZE)) + { + return -1; + } + + for (i = 0; i <= n - A52_HEADER_SIZE; i++) + { + size = a52_frame_size(&buf[i]); + if (!size) + continue; + + /* A frame at the very start is accepted as before, even if the + file holds only one frame */ + if ((pos + i == 0) + || ((lseek(fd, pos + i + size, SEEK_SET) >= 0) + && (read(fd, next, sizeof(next)) == sizeof(next)) + && a52_frame_size(next))) + { + memcpy(hdr, &buf[i], A52_HEADER_SIZE); + return pos + i; + } + } + + /* Overlap so a header split across reads is still found */ + pos += n - (A52_HEADER_SIZE - 1); + } + + return -1; +} + bool get_a52_metadata(int fd, struct mp3entry *id3) { /* Use the trackname part of the id3 structure as a temporary buffer */ unsigned char* buf = (unsigned char *)id3->path; + unsigned char hdr[A52_HEADER_SIZE]; unsigned long totalsamples; + off_t offset; int i; - if ((lseek(fd, 0, SEEK_SET) < 0) || (read(fd, buf, 5) < 5)) - { - return false; - } - - if ((buf[0] != 0x0b) || (buf[1] != 0x77)) + offset = a52_find_first_frame(fd, buf, sizeof(id3->path), hdr); + if (offset < 0) { logf("not an A52/AC3 file\n"); return false; } - i = buf[4] & 0x3e; - - if (i > 36) - { - logf("A52: Invalid frmsizecod: %d\n",i); - return false; - } + i = hdr[4] & 0x3e; id3->bitrate = a52_bitrates[i >> 1]; id3->vbr = false; id3->filesize = ffilesize(fd); + id3->first_frame_offset = offset; - switch (buf[4] & 0xc0) + switch (hdr[4] & 0xc0) { case 0x00: id3->frequency = 48000; @@ -88,15 +159,10 @@ bool get_a52_metadata(int fd, struct mp3entry *id3) id3->frequency = 32000; id3->bytesperframe = id3->bitrate * 3 * 2; break; - - default: - logf("A52: Invalid samplerate code: 0x%02x\n", buf[4] & 0xc0); - return false; - break; } /* One A52 frame contains 6 blocks, each containing 256 samples */ - totalsamples = id3->filesize / id3->bytesperframe * 6 * 256; + totalsamples = (id3->filesize - offset) / id3->bytesperframe * 6 * 256; id3->length = totalsamples / id3->frequency * 1000; return true; } From 70c5a4e9e981a877af0826d75d6839358ae94e57 Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Fri, 9 Oct 2026 14:16:49 -0400 Subject: [PATCH 407/408] FS#14031 - Update ro translations (Mihai Alexandru Vasiliu) Change-Id: Ia04e0b0c32feddb6d23e9d27f370af810e7c5bba --- apps/lang/moldoveneste.lang | 46 ++++++++++++++++++++++++++++++++----- apps/lang/romaneste.lang | 46 ++++++++++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/apps/lang/moldoveneste.lang b/apps/lang/moldoveneste.lang index 41d2163218..6ce1de413d 100644 --- a/apps/lang/moldoveneste.lang +++ b/apps/lang/moldoveneste.lang @@ -2231,15 +2231,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Retezare basuri" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Tăiere bas" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Retezare basuri" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Tăiere bas" @@ -8279,15 +8279,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Tăiere inalte" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Tăiere inalte" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Tăiere inalte" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Tăiere inalte" @@ -17132,3 +17132,37 @@ *: "Tip filtru" + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "Ieșire video compozit" + + + *: none + videoout: "Ieșire video compozit" + + + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "Scurtături gesturi touchscreen" + + + *: none + touchscreen: "Scurtături gesturi taciscrin" + + diff --git a/apps/lang/romaneste.lang b/apps/lang/romaneste.lang index 5daddd66bc..8f54582a1f 100644 --- a/apps/lang/romaneste.lang +++ b/apps/lang/romaneste.lang @@ -2231,15 +2231,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Bass Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Bass Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Retezare basuri" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Tăiere bas" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Retezare basuri" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Tăiere bas" @@ -8279,15 +8279,15 @@ user: core *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Treble Cutoff" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Treble Cutoff" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Tăiere inalte" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Tăiere înalte" *: none - gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100: "Tăiere inalte" + gigabeatfx,ipod6g,ipodnano3g,ipodnano4g,ipodvideo,mpiohd200,mpiohd300,mrobe100,samsungypcp3: "Tăiere înalte" @@ -17132,3 +17132,37 @@ *: "Tip filtru" + + id: LANG_COMPOSITE_VIDEO_OUTPUT + desc: Enables composite video framebuffer mirroring on supported targets + user: core + + *: none + videoout: "Composite Video Output" + + + *: none + videoout: "Ieșire video compozit" + + + *: none + videoout: "Ieșire video compozit" + + + + id: LANG_TOUCHSCREEN_FLICK_SHORTCUTS + desc: in settings menu + user: core + + *: none + touchscreen: "Touchscreen Flick Shortcuts" + + + *: none + touchscreen: "Scurtături gesturi touchscreen" + + + *: none + touchscreen: "Scurtături gesturi taciscrin" + + From 7d090f24b990ef54e3f9dc7eb2edba4865605471 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Wed, 7 Oct 2026 11:51:13 +0200 Subject: [PATCH 408/408] skin_engine: hold option for the peak meter bars The %pL and %pR bars followed every drop of the level, and had nothing like the peak marker of the built-in peak meter. With the new "hold" bar option a bar falls back at the peak meter's release rate, and the highest level is held for the peak hold time, both from the same settings as the built-in meter (peak_meter_get_times()). Without the option the bars are as before. The held peak is a block ending at the held level, 1 pixel thick or as many as the optional number after "hold" says (eg. "hold, 4"). On a bar with a fill image the block is drawn from the same part of the image, so a bar drawn as LED segments holds a whole segment. Co-Authored-By: Claude Opus 5.5 Change-Id: I5c6262ba02b7d2df6c5f80f15d4a1d4354ef8e26 --- apps/gui/skin_engine/skin_display.c | 83 ++++++++++++++++++++++++++++ apps/gui/skin_engine/skin_parser.c | 36 +++++++++++- apps/gui/skin_engine/wps_internals.h | 6 ++ apps/recorder/peakmeter.c | 8 +++ apps/recorder/peakmeter.h | 1 + manual/appendix/wps_tags.tex | 7 +++ 6 files changed, 139 insertions(+), 2 deletions(-) diff --git a/apps/gui/skin_engine/skin_display.c b/apps/gui/skin_engine/skin_display.c index a7c023d667..f2435fe709 100644 --- a/apps/gui/skin_engine/skin_display.c +++ b/apps/gui/skin_engine/skin_display.c @@ -170,6 +170,34 @@ void ab_draw_markers(struct screen * screen, int capacity, #endif +/* A peak meter bar with the hold option: the level falls back by the peak + * release rate - pixels per tick, here of a bar of the given size - and + * the highest recent level is held for the peak hold time. */ +static unsigned long peak_bar_hold(struct progressbar *pb, unsigned long end, + int size) +{ + int release, hold_ticks; + long delta = current_tick - pb->last_tick; + int level = end; + + peak_meter_get_times(&release, &hold_ticks); + pb->last_tick = current_tick; + if (size <= 0) + size = 1; + if (delta >= 0 && delta < HZ) + level = MAX(level, pb->level - delta * release * MAX_PEAK / size); + pb->level = level; + + if (TIME_AFTER(current_tick, pb->hold_tick)) + pb->hold_pos = 0; + if (level > pb->hold_pos) + { + pb->hold_pos = level; + pb->hold_tick = current_tick + hold_ticks; + } + return level; +} + void draw_progressbar(struct gui_wps *gwps, struct skin_viewport* skin_viewport, int line, struct progressbar *pb) { @@ -230,6 +258,8 @@ void draw_progressbar(struct gui_wps *gwps, struct skin_viewport* skin_viewport, val = pb->type == SKIN_TOKEN_PEAKMETER_LEFTBAR ? left : right; length = MAX_PEAK; end = peak_meter_scale_value(val, length); + if (pb->hold) + end = peak_bar_hold(pb, end, pb->horizontal ? width : height); } else if (pb->type == SKIN_TOKEN_PLAYLIST_PERCENTBAR) { @@ -345,6 +375,59 @@ void draw_progressbar(struct gui_wps *gwps, struct skin_viewport* skin_viewport, length, 0, end, flags); } + /* the held peak of a peak meter bar: a block hold_size pixels thick + * ending at the held level, cut from the fill image if the bar has one, + * so that a bar drawn as LED segments holds a segment */ + if (pb->hold && pb->hold_pos > 0 && + (pb->type == SKIN_TOKEN_PEAKMETER_LEFTBAR || + pb->type == SKIN_TOKEN_PEAKMETER_RIGHTBAR)) + { + struct gui_img *img = pb->nobar ? NULL : + SKINOFFSETTOPTR(get_skin_buffer(gwps->data), pb->image); + int hx = x, hy = y, hw = width, hh = height; + int len, pos, off, size = pb->hold_size; + + if (!img && !(flags&BORDER_NOFILL)) + { + /* inside the frame, like the bar */ + hx++; hy++; hw -= 2; hh -= 2; + } + len = (flags&HORIZONTAL) ? hw : hh; + pos = len * pb->hold_pos / MAX_PEAK; + off = pos - size; + if (off < 0) + { + size += off; + off = 0; + } + if (flags&INVERTFILL) + off = len - pos; + + if (flags&HORIZONTAL) + { + hx += off; + hw = size; + } + else + { + hy += off; + hh = size; + } + + if (img) + { + /* the image maps onto the bar 1:1, as in the bar itself */ + int bx = hx - x, by = hy - y; + img->bm.data = core_get_data(img->buflib_handle); + hw = MIN(hw, img->bm.width - bx); + hh = MIN(hh, img->bm.height - by); + if (hw > 0 && hh > 0) + display->bmp_part(&img->bm, bx, by, hx, hy, hw, hh); + } + else if (hw > 0 && hh > 0) + display->fillrect(hx, hy, hw, hh); + } + if (SKINOFFSETTOPTR(get_skin_buffer(gwps->data), pb->slider)) { int xoff = 0, yoff = 0; diff --git a/apps/gui/skin_engine/skin_parser.c b/apps/gui/skin_engine/skin_parser.c index 8957d9ae49..939d1ca457 100644 --- a/apps/gui/skin_engine/skin_parser.c +++ b/apps/gui/skin_engine/skin_parser.c @@ -998,6 +998,10 @@ static int parse_progressbar_tag(struct skin_element* element, pb->nofill = false; pb->noborder = false; pb->nobar = false; + pb->hold = false; + pb->hold_size = 1; + pb->level = pb->hold_pos = 0; + pb->last_tick = pb->hold_tick = 0; pb->image = PTRTOSKINOFFSET(skin_buffer, NULL); pb->slider = PTRTOSKINOFFSET(skin_buffer, NULL); pb->backdrop = PTRTOSKINOFFSET(skin_buffer, NULL); @@ -1083,14 +1087,16 @@ static int parse_progressbar_tag(struct skin_element* element, { eINVERT = 0, eNOFILL, eNOBORDER, eNOBAR, eSLIDER, eIMAGE, eBACKDROP, eVERTICAL, eHORIZONTAL, eNOTOUCH, eSETTING, eSETTING_OFFSET, - e_PB_TAG_COUNT + eHOLD, e_PB_TAG_COUNT }; static const char *pb_options[e_PB_TAG_COUNT + 1] = {[eINVERT] = "invert", [eNOFILL] = "nofill", [eNOBORDER] = "noborder", [eNOBAR] = "nobar", [eSLIDER] = "slider", [eIMAGE] = "image", [eBACKDROP] = "backdrop", [eVERTICAL] = "vertical", [eHORIZONTAL] = "horizontal", - [eNOTOUCH] = "notouch", [eSETTING] = "setting", [eSETTING_OFFSET] = "soffset", [e_PB_TAG_COUNT] = NULL}; + [eNOTOUCH] = "notouch", [eSETTING] = "setting", + [eSETTING_OFFSET] = "soffset", [eHOLD] = "hold", + [e_PB_TAG_COUNT] = NULL}; int pb_op; while (curr_param < element->params_count) @@ -1108,6 +1114,32 @@ static int parse_progressbar_tag(struct skin_element* element, pb->noborder = true; else if (pb_op == eNOBAR) pb->nobar = true; + else if (pb_op == eHOLD) + { + pb->hold = true; + /* optional: the thickness of the held peak */ + if (curr_param+1 < element->params_count) + { + struct skin_tag_parameter *next = param + 1; + int size = -1; + if (next->type == INTEGER) + size = next->data.number; + else if (next->type == STRING) + { + text = SKINOFFSETTOPTR(skin_buffer, next->data.text); + if (isdigit(*text)) + size = atoi(text); + } + if (size >= 0) + { + if (size == 0) + return WPS_ERROR_INVALID_PARAM; + pb->hold_size = size; + curr_param++; + param++; + } + } + } else if (pb_op == eSLIDER) { if (curr_param+1 < element->params_count) diff --git a/apps/gui/skin_engine/wps_internals.h b/apps/gui/skin_engine/wps_internals.h index 74c51e9994..99cf6463b3 100644 --- a/apps/gui/skin_engine/wps_internals.h +++ b/apps/gui/skin_engine/wps_internals.h @@ -136,6 +136,12 @@ struct progressbar { bool nofill; bool noborder; bool nobar; + /* peak meter bars: fall back at the peak release rate and mark the + * held peak, as the recording screen's peak meter does */ + bool hold; + int hold_size; /* thickness of the held peak, in pixels */ + int level, hold_pos; /* in units of MAX_PEAK */ + long last_tick, hold_tick; OFFSETTYPE(struct gui_img *) slider; OFFSETTYPE(struct gui_img *) backdrop; diff --git a/apps/recorder/peakmeter.c b/apps/recorder/peakmeter.c index aac1d3c077..24ee7f405e 100644 --- a/apps/recorder/peakmeter.c +++ b/apps/recorder/peakmeter.c @@ -492,6 +492,14 @@ void peak_meter_init_times(int release, int hold_ms, int clip_hold_sec) pm_clip_hold = HZ * clip_hold_sec; } +/* The release, in pixels per tick, and the peak hold time in ticks, as set + * by peak_meter_init_times(): for peak meters drawn elsewhere */ +void peak_meter_get_times(int *release, int *hold_ticks) +{ + *release = pm_peak_release; + *hold_ticks = pm_peak_hold; +} + #ifdef HAVE_RECORDING /** * Enable/disable clip counting diff --git a/apps/recorder/peakmeter.h b/apps/recorder/peakmeter.h index 8c0d0d1486..a24d64bc21 100644 --- a/apps/recorder/peakmeter.h +++ b/apps/recorder/peakmeter.h @@ -44,6 +44,7 @@ extern void peak_meter_set_clip_hold(int time); extern void peak_meter_peek(void); extern void peak_meter_init_range( bool dbfs, int range_min, int range_max); extern void peak_meter_init_times(int release, int hold_ms, int clip_hold_sec); +extern void peak_meter_get_times(int *release, int *hold_ticks); #ifdef HAVE_AGC extern void peak_meter_get_peakhold(int *peak_left, int *peak_right); #endif diff --git a/manual/appendix/wps_tags.tex b/manual/appendix/wps_tags.tex index ad56890652..89e266a877 100644 --- a/manual/appendix/wps_tags.tex +++ b/manual/appendix/wps_tags.tex @@ -810,6 +810,13 @@ display cycling round the defined sublines. See specify an image. \item[nobar] -- don't draw the bar or its frame (for use with the ``slider'' option). + \item[hold] -- for the peak meter bars (\config{\%pL}, \config{\%pR}): + the bar falls back at the peak meter's release rate instead of + following each drop, and the highest level is held for the peak hold + time. The next param may be a number, the thickness of the held peak in + pixels (default 1). With a fill image the held peak is drawn from that + part of the image, so for a bar drawn as LED segments set it to the size + of one segment. \opt{touchscreen}{ \item[notouch] -- don't create the touchregion for progress/volume bars. \item[touchlabel] -- the next option sets the label of the bar touch area, for linking with the \%Tl tag.