From 224e41e0803e8e568ba76914faedc9f034c26f1f Mon Sep 17 00:00:00 2001 From: 2b-zipper <119087427+2b-zipper@users.noreply.github.com> Date: Fri, 19 Dec 2025 21:07:43 +0900 Subject: [PATCH 1/7] Optimization of software decoder primarily for o3ds - Optimize YUV conversion with USAT/MLA instructions and register caching - Skip B-frames only to balance performance and smoothness - Use FF_IDCT_SIMPLE and disable loop filter/error concealment - Single-threaded decoding to reduce overhead This will improve audio dropouts in most 240p videos on o3ds. I never want to see assembly language again... --- .gitignore | 2 + source/network_decoder/converter.cpp | 62 +---- source/network_decoder/network_decoder.cpp | 52 ++-- source/network_decoder/yuv_converter.s | 278 +++++++++++++++++++++ source/system/test.s | 27 -- 5 files changed, 317 insertions(+), 104 deletions(-) create mode 100644 source/network_decoder/yuv_converter.s diff --git a/.gitignore b/.gitignore index 85c7e214..66e86fa0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ *.m4a* *.webm* *.s +!source/network_decoder/yuv_converter.s +!source/system/test.s *.dmp \ No newline at end of file diff --git a/source/network_decoder/converter.cpp b/source/network_decoder/converter.cpp index 5926d6ae..c8fc4cd5 100644 --- a/source/network_decoder/converter.cpp +++ b/source/network_decoder/converter.cpp @@ -129,37 +129,7 @@ Result_with_string Util_converter_yuv422_to_yuv420p(u8 *yuv422, u8 **yuv420p, in } Result_with_string Util_converter_yuv420p_to_bgr565(u8 *yuv420p, u8 **bgr565, int width, int height) { - int index = 0; - u8 *ybase = yuv420p; - u8 *ubase = yuv420p + width * height; - u8 *vbase = yuv420p + width * height + width * height / 4; - Result_with_string result; - - *bgr565 = (u8 *)malloc(width * height * 2); - if (*bgr565 == NULL) { - result.code = DEF_ERR_OUT_OF_MEMORY; - result.string = DEF_ERR_OUT_OF_MEMORY_STR; - return result; - } - - u8 Y[4], U, V, r[4], g[4], b[4]; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - // YYYYYYYYUUVV - Y[0] = ybase[x + y * width]; - U = ubase[y / 2 * width / 2 + (x / 2)]; - V = vbase[y / 2 * width / 2 + (x / 2)]; - b[0] = YUV2B(Y[0], U); - g[0] = YUV2G(Y[0], U, V); - r[0] = YUV2R(Y[0], V); - b[0] = b[0] >> 3; - g[0] = g[0] >> 2; - r[0] = r[0] >> 3; - *(*bgr565 + index++) = (g[0] & 0b00000111) << 5 | b[0]; - *(*bgr565 + index++) = (g[0] & 0b00111000) >> 3 | (r[0] & 0b00011111) << 3; - } - } - return result; + return Util_converter_yuv420p_to_bgr565_asm(yuv420p, bgr565, width, height); } Result_with_string Util_converter_yuv420p_to_bgr565_asm(u8 *yuv420p, u8 **bgr565, int width, int height) { @@ -167,6 +137,7 @@ Result_with_string Util_converter_yuv420p_to_bgr565_asm(u8 *yuv420p, u8 **bgr565 *bgr565 = (u8 *)malloc(width * height * 2); if (*bgr565 == NULL) { + logger.warning("converter", "bgr565 allocation failed for size: " + std::to_string(width * height * 2)); result.code = DEF_ERR_OUT_OF_MEMORY; result.string = DEF_ERR_OUT_OF_MEMORY_STR; return result; @@ -177,33 +148,7 @@ Result_with_string Util_converter_yuv420p_to_bgr565_asm(u8 *yuv420p, u8 **bgr565 } Result_with_string Util_converter_yuv420p_to_bgr888(u8 *yuv420p, u8 **bgr888, int width, int height) { - int index = 0; - u8 *ybase = yuv420p; - u8 *ubase = yuv420p + width * height; - u8 *vbase = yuv420p + width * height + width * height / 4; - Result_with_string result; - - *bgr888 = (u8 *)malloc(width * height * 3); - if (*bgr888 == NULL) { - result.code = DEF_ERR_OUT_OF_MEMORY; - result.string = DEF_ERR_OUT_OF_MEMORY_STR; - return result; - } - - u8 Y[4], U, V; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - // YYYYYYYYUUVV - Y[0] = *ybase++; - U = ubase[y / 2 * width / 2 + (x / 2)]; - V = vbase[y / 2 * width / 2 + (x / 2)]; - - *(*bgr888 + index++) = YUV2B(Y[0], U); - *(*bgr888 + index++) = YUV2G(Y[0], U, V); - *(*bgr888 + index++) = YUV2R(Y[0], V); - } - } - return result; + return Util_converter_yuv420p_to_bgr888_asm(yuv420p, bgr888, width, height); } Result_with_string Util_converter_yuv420p_to_bgr888_asm(u8 *yuv420p, u8 **bgr888, int width, int height) { @@ -211,6 +156,7 @@ Result_with_string Util_converter_yuv420p_to_bgr888_asm(u8 *yuv420p, u8 **bgr888 *bgr888 = (u8 *)malloc(width * height * 3); if (*bgr888 == NULL) { + logger.warning("converter", "bgr888 allocation failed for size: " + std::to_string(width * height * 3)); result.code = DEF_ERR_OUT_OF_MEMORY; result.string = DEF_ERR_OUT_OF_MEMORY_STR; return result; diff --git a/source/network_decoder/network_decoder.cpp b/source/network_decoder/network_decoder.cpp index b534540b..67bcb9f7 100644 --- a/source/network_decoder/network_decoder.cpp +++ b/source/network_decoder/network_decoder.cpp @@ -658,27 +658,36 @@ Result_with_string NetworkDecoder::init_decoder(int type) { } if ((is_av_separate() ? (type == VIDEO) : (type == BOTH))) { - decoder_context[type]->lowres = 0; - decoder_context[type]->flags = AV_CODEC_FLAG_OUTPUT_CORRUPT; - - if (codec[type]->capabilities & AV_CODEC_CAP_FRAME_THREADS) { - decoder_context[type]->thread_type = FF_THREAD_FRAME; - } else if (codec[type]->capabilities & AV_CODEC_CAP_SLICE_THREADS) { - decoder_context[type]->thread_type = FF_THREAD_SLICE; - } else { + decoder_context[type]->flags = AV_CODEC_FLAG_OUTPUT_CORRUPT | AV_CODEC_FLAG_LOW_DELAY; + decoder_context[type]->flags2 |= AV_CODEC_FLAG2_FAST; + + if (!var_is_new3ds) { + decoder_context[type]->skip_frame = AVDISCARD_NONREF; + decoder_context[type]->skip_idct = AVDISCARD_NONREF; + decoder_context[type]->skip_loop_filter = AVDISCARD_ALL; + decoder_context[type]->flags2 |= AV_CODEC_FLAG2_CHUNKS | AV_CODEC_FLAG2_SHOW_ALL; decoder_context[type]->thread_type = 0; - } - - if (decoder_context[type]->thread_type == FF_THREAD_FRAME) { - Util_fake_pthread_set_enabled_core(frame_cores_enabled); - decoder_context[type]->thread_count = - std::accumulate(std::begin(frame_cores_enabled), std::end(frame_cores_enabled), 0); - } else if (decoder_context[type]->thread_type == FF_THREAD_SLICE) { - Util_fake_pthread_set_enabled_core(slice_cores_enabled); - decoder_context[type]->thread_count = - std::accumulate(std::begin(slice_cores_enabled), std::end(slice_cores_enabled), 0); - } else { decoder_context[type]->thread_count = 1; + } else { + if (codec[type]->capabilities & AV_CODEC_CAP_FRAME_THREADS) { + decoder_context[type]->thread_type = FF_THREAD_FRAME; + } else if (codec[type]->capabilities & AV_CODEC_CAP_SLICE_THREADS) { + decoder_context[type]->thread_type = FF_THREAD_SLICE; + } else { + decoder_context[type]->thread_type = 0; + } + + if (decoder_context[type]->thread_type == FF_THREAD_FRAME) { + Util_fake_pthread_set_enabled_core(frame_cores_enabled); + decoder_context[type]->thread_count = + std::accumulate(std::begin(frame_cores_enabled), std::end(frame_cores_enabled), 0); + } else if (decoder_context[type]->thread_type == FF_THREAD_SLICE) { + Util_fake_pthread_set_enabled_core(slice_cores_enabled); + decoder_context[type]->thread_count = + std::accumulate(std::begin(slice_cores_enabled), std::end(slice_cores_enabled), 0); + } else { + decoder_context[type]->thread_count = 1; + } } } ffmpeg_result = avcodec_open2(decoder_context[type], codec[type], NULL); @@ -687,6 +696,11 @@ Result_with_string NetworkDecoder::init_decoder(int type) { goto fail; } + if (!var_is_new3ds && (is_av_separate() ? (type == VIDEO) : (type == BOTH))) { + decoder_context[type]->error_concealment = 0; + decoder_context[type]->idct_algo = FF_IDCT_SIMPLE; + } + if (type == AUDIO) { swr_context = swr_alloc(); if (!swr_context) { diff --git a/source/network_decoder/yuv_converter.s b/source/network_decoder/yuv_converter.s new file mode 100644 index 00000000..7406314b --- /dev/null +++ b/source/network_decoder/yuv_converter.s @@ -0,0 +1,278 @@ +// YUV420p to BGR565/BGR888 converter + +.data +.align 4 +.global memcpy_asm +.global memcpy_asm_4b +.global yuv420p_to_bgr565_asm +.global yuv420p_to_bgr888_asm +.type memcpy_asm, %function +.type memcpy_asm_4b, %function +.type yuv420p_to_bgr565_asm, %function +.type yuv420p_to_bgr888_asm, %function + +// ITU-R BT.709 coefficients +coef_y: .int 298 +coef_ub: .int 516 +coef_vr: .int 409 +coef_ug: .int 100 +coef_vg: .int 208 + +.text + +// r0 = dest, r1 = src, r2 = size +memcpy_asm: + push {r4-r11} + cmp r2, #64 + ble simple_copy_path + + mov r11, r0 + add r11, r2 + sub r11, #1 + + ands r3, r0, #31 + beq already_aligned + rsb r3, r3, #32 + sub r2, r2, r3 +align_bytes: + subs r3, r3, #1 + blt already_aligned + ldrb r4, [r1], #1 + strb r4, [r0], #1 + b align_bytes + +already_aligned: + pld [r1, #32] + pld [r1, #64] + pld [r1, #96] + +main_copy_loop: + cmp r0, r11 + bgt copy_finished + ldm r1!, {r3-r10} + pld [r1, #96] + stm r0!, {r3-r10} + ldm r1!, {r3-r10} + pld [r1, #96] + stm r0!, {r3-r10} + b main_copy_loop + +simple_copy_path: + cmp r2, #0 + ble copy_finished +byte_by_byte_loop: + subs r2, r2, #1 + blt copy_finished + ldrb r3, [r1], #1 + strb r3, [r0], #1 + b byte_by_byte_loop + +copy_finished: + pop {r4-r11} + bx lr + +memcpy_asm_4b: + ldr r2, [r1] + str r2, [r0] + bx lr + +// r0 = Y plane, r1 = output, r2 = width, r3 = height +yuv420p_to_bgr565_asm: + push {r4-r11, lr} + sub sp, sp, #16 + + mul r4, r2, r3 + add r5, r0, r4 + add r6, r5, r4, lsr #2 + lsr r4, r2, #1 + str r4, [sp, #0] + str r2, [sp, #4] + str r3, [sp, #8] + + ldr r7, =coef_y + ldr r7, [r7] + ldr r8, =coef_ub + ldr r8, [r8] + ldr r9, =coef_vr + ldr r9, [r9] + + mov r10, #0 + +process_row: + ldr r2, [sp, #8] + cmp r10, r2 + bge conversion_done + mov r11, #0 + +process_column: + ldr r2, [sp, #4] + cmp r11, r2 + bge next_row_565 + + lsr r4, r10, #1 + ldr r2, [sp, #0] + mul r4, r4, r2 + lsr r2, r11, #1 + add r4, r4, r2 + + ldrb r2, [r5, r4] + ldrb r3, [r6, r4] + sub r2, #128 + sub r3, #128 + + mul r12, r2, r8 + mul r14, r3, r9 + mov r4, #100 + mul r4, r2, r4 + mov r2, #208 + mla r4, r3, r2, r4 + str r4, [sp, #12] + + ldr r2, [sp, #4] + mul r3, r10, r2 + add r3, r11 + ldrb r4, [r0, r3] + sub r4, #16 + mul r4, r7 + + add r2, r4, r12 + add r2, #128 + asr r2, #8 + usat r2, #8, r2 + lsr r2, #3 + + add r3, r4, r14 + add r3, #128 + asr r3, #8 + usat r3, #8, r3 + lsr r3, #3 + + ldr r4, [sp, #12] + rsb r4, r4, #0 + ldr r12, [sp, #4] + mul r14, r10, r12 + add r14, r11 + ldrb r12, [r0, r14] + sub r12, #16 + mul r12, r7 + add r4, r12, r4 + add r4, #128 + asr r4, #8 + usat r4, #8, r4 + lsr r4, #2 + + orr r2, r2, r4, lsl #5 + orr r2, r2, r3, lsl #11 + lsl r3, r14, #1 + strh r2, [r1, r3] + + add r11, #1 + b process_column + +next_row_565: + add r10, #1 + b process_row + +conversion_done: + add sp, sp, #16 + pop {r4-r11, pc} + +// r0 = Y plane, r1 = output, r2 = width, r3 = height +yuv420p_to_bgr888_asm: + push {r4-r11, lr} + sub sp, sp, #16 + + mul r4, r2, r3 + add r5, r0, r4 + add r6, r5, r4, lsr #2 + lsr r4, r2, #1 + str r4, [sp, #0] + str r2, [sp, #4] + str r3, [sp, #8] + + ldr r7, =coef_y + ldr r7, [r7] + ldr r8, =coef_ub + ldr r8, [r8] + ldr r9, =coef_vr + ldr r9, [r9] + + mov r10, #0 + +process_row_888: + ldr r2, [sp, #8] + cmp r10, r2 + bge conversion_done_888 + mov r11, #0 + +process_column_888: + ldr r2, [sp, #4] + cmp r11, r2 + bge next_row_888 + + lsr r4, r10, #1 + ldr r2, [sp, #0] + mul r4, r4, r2 + lsr r2, r11, #1 + add r4, r4, r2 + + ldrb r2, [r5, r4] + ldrb r3, [r6, r4] + sub r2, #128 + sub r3, #128 + + mul r12, r2, r8 + mul r14, r3, r9 + mov r4, #100 + mul r4, r2, r4 + mov r2, #208 + mla r4, r3, r2, r4 + str r4, [sp, #12] + + ldr r2, [sp, #4] + mul r3, r10, r2 + add r3, r11 + ldrb r4, [r0, r3] + sub r4, #16 + mul r4, r7 + + add r2, r4, r12 + add r2, #128 + asr r2, #8 + usat r2, #8, r2 + + add r3, r4, r14 + add r3, #128 + asr r3, #8 + usat r3, #8, r3 + + ldr r4, [sp, #12] + rsb r4, r4, #0 + ldr r12, [sp, #4] + mul r14, r10, r12 + add r14, r11 + ldrb r12, [r0, r14] + sub r12, #16 + mul r12, r7 + add r4, r12, r4 + add r4, #128 + asr r4, #8 + usat r4, #8, r4 + + add r12, r14, r14, lsl #1 + strb r2, [r1, r12] + add r12, #1 + strb r4, [r1, r12] + add r12, #1 + strb r3, [r1, r12] + + add r11, #1 + b process_column_888 + +next_row_888: + add r10, #1 + b process_row_888 + +conversion_done_888: + add sp, sp, #16 + pop {r4-r11, pc} diff --git a/source/system/test.s b/source/system/test.s index c3219420..289578fd 100644 --- a/source/system/test.s +++ b/source/system/test.s @@ -1,7 +1,5 @@ .data .align 4 -.global memcpy_asm_4b -.global memcpy_asm .global test .global memory_test .global arg_test @@ -10,8 +8,6 @@ .global read_b .global read_g .global read_r -.type memcpy_asm, "function" -.type memcpy_asm_4b, "function" .type test, "function" .type memory_test, "function" .type arg_test, "function" @@ -40,29 +36,6 @@ value_0: .int 0 //#define YUV2B(Y, U) CLIP(( 298 * C(Y) + 516 * D(U) + 128) >> 8) .text -memcpy_asm: - push { r4-r11 } - mov r11, r0 - add r11, r2 - sub r11, #1 - - cpy_loop: - cmp r0, r11 - bgt cpy_end - ldm r1, { r3-r10 } - stm r0, { r3-r10 } - add r1, #32 - add r0, #32 - b cpy_loop - cpy_end: - pop { r4-r11 } - bx lr - -memcpy_asm_4b: - ldr r2, [r1] - str r2, [r0] - bx lr - test: ldr r0, =0x10000000 mov r1, #0 From a1fd38887600ebf4aee2236bf9cb82041d9847a2 Mon Sep 17 00:00:00 2001 From: 2b-zipper <119087427+2b-zipper@users.noreply.github.com> Date: Sun, 28 Dec 2025 14:07:33 +0900 Subject: [PATCH 2/7] Add user handle and subscriber count to OAuth profile display Fallback to FElibrary data if channel fetch fails (maintains previous behavior for accounts without YouTube channels) --- source/oauth/oauth.cpp | 108 ++++++++++++++++++++++++++------- source/oauth/oauth.hpp | 4 ++ source/scenes/setting_menu.cpp | 18 ++++-- 3 files changed, 104 insertions(+), 26 deletions(-) diff --git a/source/oauth/oauth.cpp b/source/oauth/oauth.cpp index c9562ad8..7b2aad64 100644 --- a/source/oauth/oauth.cpp +++ b/source/oauth/oauth.cpp @@ -31,6 +31,8 @@ std::string refresh_token = ""; std::string user_account_name = ""; std::string user_channel_id = ""; std::string user_photo_url = ""; +std::string user_handle = ""; +std::string user_subscriber_count = ""; static NetworkSessionList *session_list = nullptr; @@ -349,6 +351,8 @@ void revoke_tokens() { user_account_name = ""; user_channel_id = ""; user_photo_url = ""; + user_handle = ""; + user_subscriber_count = ""; save_tokens(); } @@ -363,6 +367,10 @@ std::string get_user_channel_id() { return user_channel_id; } std::string get_user_photo_url() { return user_photo_url; } +std::string get_user_handle() { return user_handle; } + +std::string get_user_subscriber_count() { return user_subscriber_count; } + static void encrypt_decrypt_data(std::vector &data) { size_t original_size = data.size(); size_t padded_size = (original_size + 15) & ~15; @@ -376,8 +384,9 @@ static void encrypt_decrypt_data(std::vector &data) { void save_tokens() { std::string data = "" + access_token + "\n" + "" + refresh_token + - "\n" + "" + user_account_name + "\n" + "" + - user_channel_id + "\n" + "" + user_photo_url + "\n"; + "\n" + "" + user_account_name + "\n" + "" + + user_photo_url + "\n" + "" + user_handle + "\n" + + "" + user_subscriber_count + "\n"; std::vector encrypted_data(data.begin(), data.end()); encrypt_decrypt_data(encrypted_data); @@ -410,10 +419,12 @@ void load_tokens() { size_t refresh_end = data_str.find(""); size_t name_start = data_str.find(""); size_t name_end = data_str.find(""); - size_t channel_start = data_str.find(""); - size_t channel_end = data_str.find(""); size_t photo_start = data_str.find(""); size_t photo_end = data_str.find(""); + size_t handle_start = data_str.find(""); + size_t handle_end = data_str.find(""); + size_t subscriber_start = data_str.find(""); + size_t subscriber_end = data_str.find(""); if (access_start != std::string::npos && access_end != std::string::npos) { access_start += 14; @@ -430,15 +441,20 @@ void load_tokens() { user_account_name = data_str.substr(name_start, name_end - name_start); } - if (channel_start != std::string::npos && channel_end != std::string::npos) { - channel_start += 12; - user_channel_id = data_str.substr(channel_start, channel_end - channel_start); - } - if (photo_start != std::string::npos && photo_end != std::string::npos) { photo_start += 11; user_photo_url = data_str.substr(photo_start, photo_end - photo_start); } + + if (handle_start != std::string::npos && handle_end != std::string::npos) { + handle_start += 8; + user_handle = data_str.substr(handle_start, handle_end - handle_start); + } + + if (subscriber_start != std::string::npos && subscriber_end != std::string::npos) { + subscriber_start += 18; + user_subscriber_count = data_str.substr(subscriber_start, subscriber_end - subscriber_start); + } } void fetch_library_data() { @@ -471,23 +487,71 @@ void fetch_library_data() { } auto account = header["activeAccountHeaderRenderer"]; - if (account.has_key("accountName")) { - user_account_name = youtube_parser::get_text_from_object(account["accountName"]); - } if (account.has_key("channelEndpoint")) { user_channel_id = account["channelEndpoint"]["browseEndpoint"]["browseId"].string_value(); } - if (account.has_key("accountPhoto")) { - auto thumbnails = account["accountPhoto"]["thumbnails"].array_items(); - if (!thumbnails.empty()) { - user_photo_url = thumbnails[0]["url"].string_value(); - size_t s_pos = user_photo_url.find("=s"); - if (s_pos != std::string::npos) { - size_t size_end = s_pos + 2; - while (size_end < user_photo_url.length() && isdigit(user_photo_url[size_end])) { - size_end++; + + bool channel_data_fetched = false; + + if (!user_channel_id.empty()) { + std::string channel_post_data = create_browse_request(user_channel_id); + auto channel_result = get_session().perform(HttpRequest::POST( + "https://www.youtube.com/youtubei/v1/browse", + {{"Content-Type", "application/json"}, {"Authorization", "Bearer " + access_token}}, channel_post_data)); + + if (!channel_result.fail && channel_result.status_code == 200) { + channel_result.data.push_back('\0'); + + rapidjson::Document channel_json_root; + std::string channel_error; + RJson channel_response = RJson::parse(channel_json_root, (char *)channel_result.data.data(), channel_error); + + if (channel_error.empty() && channel_response.has_key("header")) { + auto channel_header = channel_response["header"]; + if (channel_header.has_key("c4TabbedHeaderRenderer")) { + auto tabbed_header = channel_header["c4TabbedHeaderRenderer"]; + + if (tabbed_header.has_key("title")) { + user_account_name = tabbed_header["title"].string_value(); + channel_data_fetched = true; + } + + if (tabbed_header.has_key("channelHandleText")) { + user_handle = youtube_parser::get_text_from_object(tabbed_header["channelHandleText"]); + } + + if (tabbed_header.has_key("subscriberCountText")) { + user_subscriber_count = + youtube_parser::get_text_from_object(tabbed_header["subscriberCountText"]); + } + + if (tabbed_header.has_key("avatar")) { + auto thumbnails = tabbed_header["avatar"]["thumbnails"].array_items(); + if (!thumbnails.empty()) { + user_photo_url = thumbnails[thumbnails.size() - 1]["url"].string_value(); + } + } + } + } + } + } + + if (!channel_data_fetched) { + if (account.has_key("accountName")) { + user_account_name = youtube_parser::get_text_from_object(account["accountName"]); + } + if (account.has_key("accountPhoto")) { + auto thumbnails = account["accountPhoto"]["thumbnails"].array_items(); + if (!thumbnails.empty()) { + user_photo_url = thumbnails[0]["url"].string_value(); + size_t s_pos = user_photo_url.find("=s"); + if (s_pos != std::string::npos) { + size_t size_end = s_pos + 2; + while (size_end < user_photo_url.length() && isdigit(user_photo_url[size_end])) { + size_end++; + } + user_photo_url = user_photo_url.substr(0, size_end) + "-c-k-c0x00ffffff-no-rj"; } - user_photo_url = user_photo_url.substr(0, size_end) + "-c-k-c0x00ffffff-no-rj"; } } } diff --git a/source/oauth/oauth.hpp b/source/oauth/oauth.hpp index b6ebf868..026ddf83 100644 --- a/source/oauth/oauth.hpp +++ b/source/oauth/oauth.hpp @@ -23,6 +23,8 @@ extern std::string refresh_token; extern std::string user_account_name; extern std::string user_channel_id; extern std::string user_photo_url; +extern std::string user_handle; +extern std::string user_subscriber_count; void init(); void exit(); @@ -35,6 +37,8 @@ std::string get_access_token(); std::string get_user_account_name(); std::string get_user_channel_id(); std::string get_user_photo_url(); +std::string get_user_handle(); +std::string get_user_subscriber_count(); void save_tokens(); void load_tokens(); diff --git a/source/scenes/setting_menu.cpp b/source/scenes/setting_menu.cpp index 9b26ac23..b558dcf1 100644 --- a/source/scenes/setting_menu.cpp +++ b/source/scenes/setting_menu.cpp @@ -338,11 +338,16 @@ static void oauth_worker_thread_func(void *) { resource_lock.lock(); if (oauth_user_view) { std::string user_name = OAuth::get_user_account_name(); - std::string channel_id = OAuth::get_user_channel_id(); + std::string handle = OAuth::get_user_handle(); + std::string subscriber_count = OAuth::get_user_subscriber_count(); std::string photo_url = OAuth::get_user_photo_url(); + std::vector aux_lines; + if (!handle.empty()) aux_lines.push_back(handle); + if (!subscriber_count.empty()) aux_lines.push_back(subscriber_count); + oauth_user_view->set_name(user_name); - oauth_user_view->set_auxiliary_lines({channel_id}); + oauth_user_view->set_auxiliary_lines(aux_lines); oauth_user_view->set_thumbnail_url(photo_url); oauth_user_view->set_height(CHANNEL_ICON_HEIGHT); if (!photo_url.empty()) { @@ -940,12 +945,17 @@ void Sem_init(void) { var_oauth_enabled = true; std::string user_name = OAuth::get_user_account_name(); - std::string channel_id = OAuth::get_user_channel_id(); + std::string handle = OAuth::get_user_handle(); + std::string subscriber_count = OAuth::get_user_subscriber_count(); std::string photo_url = OAuth::get_user_photo_url(); if (oauth_user_view && !user_name.empty()) { + std::vector aux_lines; + if (!handle.empty()) aux_lines.push_back(handle); + if (!subscriber_count.empty()) aux_lines.push_back(subscriber_count); + oauth_user_view->set_name(user_name); - oauth_user_view->set_auxiliary_lines({channel_id}); + oauth_user_view->set_auxiliary_lines(aux_lines); oauth_user_view->set_thumbnail_url(photo_url); oauth_user_view->set_height(CHANNEL_ICON_HEIGHT); if (!photo_url.empty()) { From 406cf1d129da7cb071d5af43a7a1053e4343e806 Mon Sep 17 00:00:00 2001 From: 2b-zipper <119087427+2b-zipper@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:04:42 +0900 Subject: [PATCH 3/7] Enable pull to refresh even when content fails to load --- source/ui/views/scroll.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/ui/views/scroll.cpp b/source/ui/views/scroll.cpp index 566f55bd..e29acb4b 100644 --- a/source/ui/views/scroll.cpp +++ b/source/ui/views/scroll.cpp @@ -16,8 +16,9 @@ void ScrollView::update_scroller(Hid_info key) { if (key.p_touch) { first_touch_x = key.touch_x; first_touch_y = key.touch_y; + double touch_area_bottom = (pull_to_refresh_enabled && content_height == 0) ? y1 : std::min(y0 + content_height, y1); if (x0 <= key.touch_x && key.touch_x < x1 && y0 <= key.touch_y && - key.touch_y < std::min(y0 + content_height, y1) && + key.touch_y < touch_area_bottom && (var_time_to_turn_off_lcd == 0 || var_afk_time <= var_time_to_turn_off_lcd)) { grabbed = true; } From 21ab793c3cc208eaa543621474c026f0fc5744ed Mon Sep 17 00:00:00 2001 From: 2b-zipper <119087427+2b-zipper@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:27:23 +0900 Subject: [PATCH 4/7] Allow OAuth authentication retry on error Display "Retry" button text when OAuth is in ERROR state. Pressing the button calls refresh_access_token() to retry authentication without requiring app restart or clearing existing tokens, though this should rarely happen. --- source/scenes/setting_menu.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/source/scenes/setting_menu.cpp b/source/scenes/setting_menu.cpp index 6422740f..f9d91728 100644 --- a/source/scenes/setting_menu.cpp +++ b/source/scenes/setting_menu.cpp @@ -868,8 +868,13 @@ void Sem_init(void) { (new EmptyView(0, 0, 320, SMALL_MARGIN)), (new TextView(10, 0, 120, DEFAULT_FONT_INTERVAL + SMALL_MARGIN * 2)) ->set_text([] () { - return OAuth::oauth_state == OAuth::OAuthState::AUTHENTICATED ? - LOCALIZED(OAUTH_LOGOUT) : LOCALIZED(OAUTH_LOGIN); + if (OAuth::oauth_state == OAuth::OAuthState::AUTHENTICATED) { + return LOCALIZED(OAUTH_LOGOUT); + } else if (OAuth::oauth_state == OAuth::OAuthState::ERROR) { + return LOCALIZED(RETRY); + } else { + return LOCALIZED(OAUTH_LOGIN); + } }) ->set_x_alignment(TextView::XAlign::CENTER) ->set_text_offset(0, -2) @@ -896,6 +901,8 @@ void Sem_init(void) { oauth_user_view->thumbnail_handle = -1; oauth_user_view->set_height(0); } + } else if (OAuth::oauth_state == OAuth::OAuthState::ERROR) { + OAuth::refresh_access_token(); } else if (OAuth::oauth_state == OAuth::OAuthState::NOT_AUTHENTICATED) { OAuth::start_device_flow(); if (OAuth::oauth_state == OAuth::OAuthState::AUTHENTICATING) { From 43ccba6f81e8569699668b64e2fc245e960ec448 Mon Sep 17 00:00:00 2001 From: 2b-zipper <119087427+2b-zipper@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:32:54 +0900 Subject: [PATCH 5/7] Revert to multithreading Let's revert that stupid change --- source/network_decoder/network_decoder.cpp | 49 +++++++++------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/source/network_decoder/network_decoder.cpp b/source/network_decoder/network_decoder.cpp index 4fef59a6..81d1967e 100644 --- a/source/network_decoder/network_decoder.cpp +++ b/source/network_decoder/network_decoder.cpp @@ -658,36 +658,32 @@ Result_with_string NetworkDecoder::init_decoder(int type) { } if ((is_av_separate() ? (type == VIDEO) : (type == BOTH))) { - decoder_context[type]->flags = AV_CODEC_FLAG_OUTPUT_CORRUPT | AV_CODEC_FLAG_LOW_DELAY; - decoder_context[type]->flags2 |= AV_CODEC_FLAG2_FAST; + decoder_context[type]->lowres = 0; + decoder_context[type]->flags = AV_CODEC_FLAG_OUTPUT_CORRUPT; if (!var_is_new3ds) { decoder_context[type]->skip_frame = AVDISCARD_NONREF; - decoder_context[type]->skip_idct = AVDISCARD_NONREF; decoder_context[type]->skip_loop_filter = AVDISCARD_ALL; - decoder_context[type]->flags2 |= AV_CODEC_FLAG2_CHUNKS | AV_CODEC_FLAG2_SHOW_ALL; - decoder_context[type]->thread_type = 0; - decoder_context[type]->thread_count = 1; + } + + if (codec[type]->capabilities & AV_CODEC_CAP_FRAME_THREADS) { + decoder_context[type]->thread_type = FF_THREAD_FRAME; + } else if (codec[type]->capabilities & AV_CODEC_CAP_SLICE_THREADS) { + decoder_context[type]->thread_type = FF_THREAD_SLICE; } else { - if (codec[type]->capabilities & AV_CODEC_CAP_FRAME_THREADS) { - decoder_context[type]->thread_type = FF_THREAD_FRAME; - } else if (codec[type]->capabilities & AV_CODEC_CAP_SLICE_THREADS) { - decoder_context[type]->thread_type = FF_THREAD_SLICE; - } else { - decoder_context[type]->thread_type = 0; - } + decoder_context[type]->thread_type = 0; + } - if (decoder_context[type]->thread_type == FF_THREAD_FRAME) { - Util_fake_pthread_set_enabled_core(frame_cores_enabled); - decoder_context[type]->thread_count = - std::accumulate(std::begin(frame_cores_enabled), std::end(frame_cores_enabled), 0); - } else if (decoder_context[type]->thread_type == FF_THREAD_SLICE) { - Util_fake_pthread_set_enabled_core(slice_cores_enabled); - decoder_context[type]->thread_count = - std::accumulate(std::begin(slice_cores_enabled), std::end(slice_cores_enabled), 0); - } else { - decoder_context[type]->thread_count = 1; - } + if (decoder_context[type]->thread_type == FF_THREAD_FRAME) { + Util_fake_pthread_set_enabled_core(frame_cores_enabled); + decoder_context[type]->thread_count = + std::accumulate(std::begin(frame_cores_enabled), std::end(frame_cores_enabled), 0); + } else if (decoder_context[type]->thread_type == FF_THREAD_SLICE) { + Util_fake_pthread_set_enabled_core(slice_cores_enabled); + decoder_context[type]->thread_count = + std::accumulate(std::begin(slice_cores_enabled), std::end(slice_cores_enabled), 0); + } else { + decoder_context[type]->thread_count = 1; } } ffmpeg_result = avcodec_open2(decoder_context[type], codec[type], NULL); @@ -696,11 +692,6 @@ Result_with_string NetworkDecoder::init_decoder(int type) { goto fail; } - if (!var_is_new3ds && (is_av_separate() ? (type == VIDEO) : (type == BOTH))) { - decoder_context[type]->error_concealment = 0; - decoder_context[type]->idct_algo = FF_IDCT_SIMPLE; - } - if (type == AUDIO) { swr_context = swr_alloc(); if (!swr_context) { From 95361d6b351d89f22ae53cb61e6b71d08f78da5a Mon Sep 17 00:00:00 2001 From: 2b-zipper <119087427+2b-zipper@users.noreply.github.com> Date: Mon, 29 Dec 2025 23:02:29 +0900 Subject: [PATCH 6/7] Add reload banner to account subscriptions tab when pull-to-refresh is disabled --- source/scenes/home.cpp | 95 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 11 deletions(-) diff --git a/source/scenes/home.cpp b/source/scenes/home.cpp index c780ece5..b9037178 100644 --- a/source/scenes/home.cpp +++ b/source/scenes/home.cpp @@ -105,11 +105,48 @@ void Home_init(void) { }); if (OAuth::is_authenticated()) { - channels_tab_view = (new TabView(0, 0, 320, 0)) - ->set_views({oauth_channels_tab_view, local_channels_tab_view}) - ->set_tab_texts>( - {[]() { return LOCALIZED(ACCOUNT); }, []() { return LOCALIZED(LOCAL_CHANNELS); }}) - ->set_lr_tab_switch_enabled(false); + if (var_disable_pull_to_refresh) { + View *oauth_channels_view = + (new VerticalListView(0, 0, 320)) + ->set_views({(new TextView(0, 0, 320, FEED_RELOAD_BUTTON_HEIGHT)) + ->set_text((std::function)[]() { + auto res = LOCALIZED(RELOAD); + if (is_async_task_running(load_oauth_subscribed_channels)) { + res += " ..."; + } + return res; + }) + ->set_text_offset(SMALL_MARGIN, -1) + ->set_on_view_released([](View &) { + if (!is_async_task_running(load_oauth_subscribed_channels)) { + queue_async_task(load_oauth_subscribed_channels, NULL); + } + }) + ->set_get_background_color([](const View &view) -> u32 { + if (is_async_task_running(load_oauth_subscribed_channels)) { + return LIGHT0_BACK_COLOR; + } + return View::STANDARD_BACKGROUND(view); + }), + (new RuleView(0, 0, 320, SMALL_MARGIN)) + ->set_margin(0) + ->set_get_background_color([](const View &) { return DEFAULT_BACK_COLOR; }), + oauth_channels_tab_view}) + ->set_draw_order({2, 1, 0}); + channels_tab_view = + (new TabView(0, 0, 320, 0)) + ->set_views({oauth_channels_view, local_channels_tab_view}) + ->set_tab_texts>( + {[]() { return LOCALIZED(ACCOUNT); }, []() { return LOCALIZED(LOCAL_CHANNELS); }}) + ->set_lr_tab_switch_enabled(false); + } else { + channels_tab_view = + (new TabView(0, 0, 320, 0)) + ->set_views({oauth_channels_tab_view, local_channels_tab_view}) + ->set_tab_texts>( + {[]() { return LOCALIZED(ACCOUNT); }, []() { return LOCALIZED(LOCAL_CHANNELS); }}) + ->set_lr_tab_switch_enabled(false); + } } else { channels_tab_view = local_channels_tab_view; } @@ -306,12 +343,48 @@ void Home_rebuild_channels_tab(void) { }); if (OAuth::is_authenticated()) { - channels_tab_view = - (new TabView(0, 0, 320, 0)) - ->set_views({oauth_channels_tab_view, local_channels_tab_view}) - ->set_tab_texts>( - {[]() { return LOCALIZED(ACCOUNT); }, []() { return LOCALIZED(LOCAL_CHANNELS); }}) - ->set_lr_tab_switch_enabled(false); + if (var_disable_pull_to_refresh) { + View *oauth_channels_view = + (new VerticalListView(0, 0, 320)) + ->set_views({(new TextView(0, 0, 320, FEED_RELOAD_BUTTON_HEIGHT)) + ->set_text((std::function)[]() { + auto res = LOCALIZED(RELOAD); + if (is_async_task_running(load_oauth_subscribed_channels)) { + res += " ..."; + } + return res; + }) + ->set_text_offset(SMALL_MARGIN, -1) + ->set_on_view_released([](View &) { + if (!is_async_task_running(load_oauth_subscribed_channels)) { + queue_async_task(load_oauth_subscribed_channels, NULL); + } + }) + ->set_get_background_color([](const View &view) -> u32 { + if (is_async_task_running(load_oauth_subscribed_channels)) { + return LIGHT0_BACK_COLOR; + } + return View::STANDARD_BACKGROUND(view); + }), + (new RuleView(0, 0, 320, SMALL_MARGIN)) + ->set_margin(0) + ->set_get_background_color([](const View &) { return DEFAULT_BACK_COLOR; }), + oauth_channels_tab_view}) + ->set_draw_order({2, 1, 0}); + channels_tab_view = + (new TabView(0, 0, 320, 0)) + ->set_views({oauth_channels_view, local_channels_tab_view}) + ->set_tab_texts>( + {[]() { return LOCALIZED(ACCOUNT); }, []() { return LOCALIZED(LOCAL_CHANNELS); }}) + ->set_lr_tab_switch_enabled(false); + } else { + channels_tab_view = + (new TabView(0, 0, 320, 0)) + ->set_views({oauth_channels_tab_view, local_channels_tab_view}) + ->set_tab_texts>( + {[]() { return LOCALIZED(ACCOUNT); }, []() { return LOCALIZED(LOCAL_CHANNELS); }}) + ->set_lr_tab_switch_enabled(false); + } update_oauth_subscribed_channels(get_oauth_subscribed_channels()); } else { channels_tab_view = local_channels_tab_view; From fb180a74f7ef2482831ca39906f468b4cc21a942 Mon Sep 17 00:00:00 2001 From: 2b-zipper <119087427+2b-zipper@users.noreply.github.com> Date: Mon, 29 Dec 2025 23:48:35 +0900 Subject: [PATCH 7/7] Increase search text limit to 256 characters 32 characters was too few --- source/scenes/search.cpp | 67 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/source/scenes/search.cpp b/source/scenes/search.cpp index 576201fe..cb053d45 100644 --- a/source/scenes/search.cpp +++ b/source/scenes/search.cpp @@ -28,6 +28,62 @@ bool exiting = false; Mutex resource_lock; std::string cur_search_word = ""; + +static size_t get_next_utf8_char_pos(const std::string &text, size_t pos) { + if (pos >= text.length()) { + return text.length(); + } + + unsigned char c = text[pos]; + if ((c & 0x80) == 0) { + return pos + 1; + } else if ((c & 0xE0) == 0xC0) { + return pos + 2; + } else if ((c & 0xF0) == 0xE0) { + return pos + 3; + } else if ((c & 0xF8) == 0xF0) { + return pos + 4; + } + return pos + 1; +} + +std::string truncate_search_text_for_display(const std::string &text, double max_width, double font_size) { + float text_width = Draw_get_width(text, font_size); + + if (text_width <= max_width - SMALL_MARGIN * 2) { + return text; + } + + std::string ellipsis = "..."; + float ellipsis_width = Draw_get_width(ellipsis, font_size); + float available_width = max_width - SMALL_MARGIN * 2 - ellipsis_width; + + size_t pos = 0; + size_t last_valid_pos = 0; + + while (pos < text.length()) { + size_t next_pos = get_next_utf8_char_pos(text, pos); + if (next_pos > text.length()) { + break; + } + + std::string substr = text.substr(0, next_pos); + float substr_width = Draw_get_width(substr, font_size); + + if (substr_width <= available_width) { + last_valid_pos = next_pos; + pos = next_pos; + } else { + break; + } + } + + if (last_valid_pos > 0 && last_valid_pos < text.length()) { + return text.substr(0, last_valid_pos) + ellipsis; + } + + return text; +} YouTubeSearchResult search_result; bool search_done = false; @@ -334,22 +390,25 @@ bool Search_show_search_keyboard() { resource_lock.lock(); } SwkbdState keyboard; - swkbdInit(&keyboard, SWKBD_TYPE_NORMAL, 2, 32); + swkbdInit(&keyboard, SWKBD_TYPE_NORMAL, 2, 256); swkbdSetFeatures(&keyboard, SWKBD_DEFAULT_QWERTY | SWKBD_PREDICTIVE_INPUT); swkbdSetValidation(&keyboard, SWKBD_NOTEMPTY_NOTBLANK, 0, 0); swkbdSetButton(&keyboard, SWKBD_BUTTON_LEFT, LOCALIZED(CANCEL).c_str(), false); swkbdSetButton(&keyboard, SWKBD_BUTTON_RIGHT, LOCALIZED(OK).c_str(), true); swkbdSetInitialText(&keyboard, cur_search_word.c_str()); - char search_word[129]; + char search_word[257]; add_cpu_limit(40); video_set_skip_drawing(true); - auto button_pressed = swkbdInputText(&keyboard, search_word, 64); + auto button_pressed = swkbdInputText(&keyboard, search_word, 256); video_set_skip_drawing(false); remove_cpu_limit(40); if (button_pressed == SWKBD_BUTTON_RIGHT) { cur_search_word = search_word; - search_box_view->set_text(search_word); + // Truncate text to fit in search box and display + double search_box_width = 320 - SEARCH_BOX_MARGIN * 3 - URL_BUTTON_WIDTH; + std::string display_text = truncate_search_text_for_display(search_word, search_box_width, 0.5); + search_box_view->set_text(display_text); search_box_view->set_get_text_color([]() { return DEFAULT_TEXT_COLOR; }); remove_all_async_tasks_with_type(load_search_results);