找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 275|回复: 10

【Win32】Windows 解决拖拽窗口位置和大小时造成窗口消息阻塞的问题

[复制链接]
发表于 4 天前 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×

Windows 解决拖拽窗口位置和大小时造成窗口消息阻塞的问题

前因后果

当你开始拖拽窗口标题栏进行移动,或者窗口调整大小的边框进行窗口大小调整的时候,首先你会收到 WM_ENTERSIZEMOVE 消息。这个消息仅仅是告诉你要开始拖拽了。然后,你会收到 WM_SYSCOMMAND 消息,其中 wParam 的值决定了它要干啥。此时,你要用 switch (GET_SC_WPARAM(wParam)) 判断它要干啥。当你 case SC_MOVE: 或者 case SC_SIZE: 的时候,就说明,它要进入拖拽模式了。而如果此时你直接把消息转发给 DefWindowProc(),它就会 阻塞,在它的内部建立 它自己的消息循环,然后它内部处理你鼠标的移动、窗口大小的变化等等复杂的内容。当你松开了鼠标,DefWindowProc() 函数返回 0结束了阻塞。最后,它发送给你一个  WM_EXITSIZEMOVE,告诉你退出了拖拽模式。

对于游戏开发、视频播放器开发等各种既不想使用阻塞式 GetMessage() + 定时触发式 Timer 的场合,又不能在处理消息的时候遇到阻塞(比如 DefWindowProc() 阻塞),但是你又希望你的窗口能够被拖拽或者调整大小的时候,我这里有一个 黑科技 可以做到中途取消阻塞。

解决思路

接口设计

首先我的窗口消息处理的函数是非阻塞的,它大致就叫 PollWindowEvents(),供调用者循环调用。调用者在自己的循环里处理自己的事务,比如进行动态画面的渲染,完事儿后调用一次 PollWindowEvents() 让我这边再处理窗口消息的事务,可以获取键盘输入等,就像 GLFW 那样,只要我的 PollWindowEvents() 不阻塞,调用者的循环就可以持续进行。

我的 PollWindowEvents() 的大致逻辑是:用 while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) 把消息队列里面的所有消息全部取出来处理,然后返回。处理的方式就是调用 TranslateMessage()DispatchMessageW()。检测到有 WM_QUIT 的时候,我设置一个 should_quit 变量的值为非零,这样调用者就知道可以退出循环了(因为大概窗口已经被 X 掉了)

差不多长这样:

while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE))
{
    TranslateMessage(&msg);
    if (msg.message == WM_QUIT)
    {
        w->should_quit = 1;
        w->exit_code = (int)msg.wParam;
    }
    else
    {
        DispatchMessageW(&msg);
    }
}

切入点

拖拽窗口的时候,虽然 DefWindowProc() 阻塞住了,但是计时器事件仍然能传递到你的窗口消息处理过程中,也就是 WM_TIMER。除了计时器事件,WM_MOVEWM_SIZEWM_SIZING 事件也会在拖拽窗口、调整窗口大小的时候传递到你的窗口处理过程里,而且频率很高。

如果在这些消息事件的里面,我如果能跳转到 DefWindowProc() 的外面,然后让我的 PollWindowEvents() 返回,这样不就能解决阻塞的问题了吗?等到用户的下一个循环调用 PollWindowEvents() 的时候,我再跳转到我的消息事件处理函数里面,就当没事发生一样,那就能回到 DefWindowProc() 处理鼠标拖拽窗口的逻辑上去,成功闭环。

理论实施

能做到这种跳转的手段,目前 C 标准库里提供的是 setjmp()longjmp()可惜不好用。因为 Windows 的 MSVC 的 longjmp() 并不是纯 C 的行为,它会通过 Unwind() 等方式,在 C++ 环境下,严格执行 RAII 资源释放规则,并且还会检测抛出后未处理的异常,来保证在 C++ 里使用它是「安全」的。它认为你调用 longjmp() 后你就一去不复返了,那么当前上下文里面的资源释放和异常处理都需要全部做完了才能允许你跳走。这种 「安全处理」反而是我不需要的,因为我其实会跳转回来,这样的话,也不妨碍它继续按正常流程处理 RAII 资源释放规则和异常处理规则。

能做到跳转是其次,关键的问题在于:正在阻塞的 DefWindowProc() 会使用 ,而你如果跳出了它的内部循环出来了,并从 PollWindowEvents() 返回了的时候,你的调用者也需要使用栈。而两者的栈深度是不一样的,正在阻塞的 DefWindowProc() 位于栈的最顶部,而调用者的循环和它的处理逻辑则在较浅的位置上使用栈(longjmp() 会把 栈寄存器 恢复到 setjmp() 时的状态),导致本该是后进先出的栈,它的前面和后面被同时使用,造成数据的覆盖和栈帧的损坏。因此必须要有两个栈。

逻辑框架

实现不受限制的跳转函数,以及切换栈。

  • C 标准库的 longjmp() 做了多余的安全考虑。自己造轮子 去除它的多余的安全处理。
  • 使用 _aligned_malloc() 分配内存用作新栈。必须以 16 字节对齐
  • 使用编写跳板函数,取名为 jmp_to_new_stack(),它干这三件事:
    • 改变栈寄存器到新栈的栈顶。
    • 调用一个回调函数,这个回调函数负责在新栈上调用指定的函数。
    • 使用 自己造的 my_longjmp() 从新栈上返回到旧栈上的 setjmp() 返回点。

为啥要用 my_longjmp() 而不是直接切回旧的栈寄存器值呢?因为你的程序在同时使用两个栈,而两个栈的栈顶是随时变化的,旧的栈寄存器值在你的函数发生返回或者跳转后就已经是野指针了,因为栈顶变化了。

设置跳转目标:

  • PollWindowEvents() 的开头部分,也就是在 while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) 之前,用 setjmp() 设置一个跳转目标,这是用来实现重入逻辑的。
  • PollWindowEvents() 的结尾部分,也就是 while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) 的循环体之后,用 setjmp() 设置一个跳转目标,这是用来实现逃离阻塞用的,也就是让你的 PollWindowEvents() 可以假装是一个非阻塞函数,在此返回。
  • WM_TIMERWM_MOVEWM_SIZE 这些消息的处理过程里,用 setjmp() 设置跳转目标,用于重新返回到阻塞函数的内部,使其继续运行。

进入阻塞:

  • WM_SYSCOMMAND 消息里判断它是不是要 SC_MOVESC_SIZE 了,如果是,那就创建定时器,给新栈分配内存,调用 jmp_to_new_stack() 进入新栈,指定的回调函数专门就负责调用 DefWindowProcW(w->Window, WM_SYSCOMMAND, 它的 wParam 值, 0);,进入阻塞。

逃离阻塞:

  • WM_TIMERWM_MOVEWM_SIZE 消息里,此时你在新栈上,你需要用 my_longjmp() 跳转到 PollWindowEvents() 结尾部分的跳转目标,实现逃离阻塞,逃离到你的调用者的那边去。

重入逻辑:

  • 因为你在处理 WM_TIMERWM_MOVEWM_SIZE 这些消息的一半的时候就跑了,而且用的是我的 my_longjmp(),既没有执行 RAII 的资源释放规则,又没有处理抛出的异常,你肯定是要回来让它继续把这些东西处理完的。很简单,当你的 PollWindowEvents() 再次被你的调用者调用的时候,你通过你设置的变量来判断你当前的状态是不是中途逃离阻塞的状态,如果是,使用 my_longjmp() 跳转回到 WM_TIMERWM_MOVEWM_SIZE 消息里设置的跳转目标即可。

退出阻塞:

  • 当造成阻塞的  DefWindowProcW(w->Window, WM_SYSCOMMAND, 它的 wParam 值, 0); 返回的时候,你需要使用 my_longjmp() 跳转回到 PollWindowEvents() 的开头部分,这样它就能继续处理完其它的消息了。

代码实现

首先是我们的最最关键的两个函数的实现:

  • _my_longjmp()
    __declspec(noreturn)
    void _my_longjmp(jmp_buf jb, int value)
    {
    #ifdef _M_IX86
        static const uint32_t shellcode_my_longjmp[] =
        {
            0x0424548B,
            0x0824448B,
            0x8301F883,
            0x2A8B00D0,
            0x8B045A8B,
            0x728B087A,
            0x10628B0C,
            0xFF04C483,
            0x90901462,
        };
    #elif defined(_M_X64)
        static const uint64_t shellcode_my_longjmp[] =
        {
            0x4808598B48D08948,
            0x4818698B4810618B,
            0x4C28798B4820718B,
            0x4C38698B4C30618B,
            0x0F48798B4C40718B,
            0x5C69D9E2DB5851AE,
            0x6F0F6660716F0F66,
            0x80816F0F44667079,
            0x896F0F4466000000,
            0x6F0F446600000090,
            0x0F4466000000A091,
            0x4466000000B0996F,
            0x66000000C0A16F0F,
            0x000000D0A96F0F44,
            0x0000E0B16F0F4466,
            0x00F0B96F0F446600,
            0x9090905061FF0000,
        };
    #else
        // If your computer is not x86 nor x64, implement your own `longjmp()` shellcode for your CPU.
        longjmp(jb, value);
    #endif
        static DWORD dwOldProtect = 0;
        if (!dwOldProtect) VirtualProtect((void *)shellcode_my_longjmp, sizeof shellcode_my_longjmp, PAGE_EXECUTE_READ, &dwOldProtect);
        void(*my_longjmp)(jmp_buf jb, int value) = (void *)shellcode_my_longjmp;
        my_longjmp(jb, value);
    }
  • _jmp_to_new_stack()
    __declspec(noreturn)
    void _jmp_to_new_stack(void *stack_buffer, size_t stack_size, void(*function_to_run)(void *userdata), void *userdata, jmp_buf returning, int longjmp_val)
    {
    #ifdef _M_IX86
        static const uint32_t shellcode_jmp_to_new_stack[] =
        {
            0x608be089,
            0x08600304,
            0xff1870ff,
            0x70ff1470,
            0x0c50ff10,
            0x6804c483,
            0xdeadbeef,
            0x909002eb,
            0x0424548b,
            0x0824448b,
            0x8301f883,
            0x2a8b00d0,
            0x8b045a8b,
            0x728b087a,
            0x10628b0c,
            0xff04c483,
            0x90901462,
        };
    #elif defined(_M_X64)
        static const uint64_t shellcode_jmp_to_new_stack[] =
        {
            0x0148cc8948e08948,
            0x4c2870ff3070ffd4,
            0xff4120ec8348c989,
            0xeb5a5920c48348d0,
            0x9090909090909007,
            0x4808598b48d08948,
            0x4818698b4810618b,
            0x4c28798b4820718b,
            0x4c38698b4c30618b,
            0x0f48798b4c40718b,
            0x5c69d9e2db5851ae,
            0x6f0f6660716f0f66,
            0x80816f0f44667079,
            0x896f0f4466000000,
            0x6f0f446600000090,
            0x0f4466000000a091,
            0x4466000000b0996f,
            0x66000000c0a16f0f,
            0x000000d0a96f0f44,
            0x0000e0b16f0f4466,
            0x00f0b96f0f446600,
            0x9090905061ff0000,
        };
    #else
        fprintf(stderr, "[UNIMPLEMENTED] Please provide your shellcode for your CPU to implement `jmp_to_new_stack()` by doing these steps:\n");
        fprintf(stderr, "[UNIMPLEMENTED] Save your current stack pointer register to a volatile register whichever you'd like to use, the volatile register stores your original stack pointer and could help you to retrieve your parameters;\n");
        fprintf(stderr, "[UNIMPLEMENTED] Set your stack pointer register to the end of my stack buffer: `(size_t)stack_buffer + stack_size`;\n");
        fprintf(stderr, "[UNIMPLEMENTED] After moving to the new stack, save your 5th and 6th paramters to the new stack;\n");
        fprintf(stderr, "[UNIMPLEMENTED] Retrieve your 4th parameter `userdata` from the original stack (using the saved stack pointer in the volatile register);\n");
        fprintf(stderr, "[UNIMPLEMENTED] Your 3rd parameter is a pointer to a callback function (the function to run on the new stack). Call it and pass `userdata`. NOTE: This function will destroy your volatile register as usual occasion;\n");
        fprintf(stderr, "[UNIMPLEMENTED] After calling the function, balance your stack;\n");
        fprintf(stderr, "[UNIMPLEMENTED] Retrieve your 5th (`jmp_buf`) and 6th (`longjmp_value`) parameters from where you saved them on the new stack, these parameters are for returning to your previous stack via a `longjmp()`;\n");
        fprintf(stderr, "[UNIMPLEMENTED] IMPORTANT: Both stacks are actively changing, do not attempt to restore the stack pointer directly;\n");
        fprintf(stderr, "[UNIMPLEMENTED] Implement and execute a `longjmp(jmp_buf, longjmp_value)` to return to the original stack\n");
        assert(0);
    #endif
        static DWORD dwOldProtect = 0;
        if (!dwOldProtect) VirtualProtect((void *)shellcode_jmp_to_new_stack, sizeof shellcode_jmp_to_new_stack, PAGE_EXECUTE_READ, &dwOldProtect);
        void(*jmp_to_new_stack)(void *, size_t, void(*)(void *), void *, jmp_buf, int) = (void *)shellcode_jmp_to_new_stack;
        jmp_to_new_stack(stack_buffer, stack_size, function_to_run, userdata, returning, longjmp_val);
    }

    你也许会在想,这些整整齐齐的数组是什么呢?当然是 shellcode 啦,也就是用汇编写的跳板函数。原理也很简单。

  • Gitee 仓库:https://gitee.com/a5k3rn3l/cdecl_jump_to_new_stack
  • github 仓库:https://github.com/0xAA55/cdecl_jump_to_new_stack

两个仓库是完全一样,同步更新的。现在来介绍这些 shellcode 是干嘛的。

  • x86 版_my_longjmp

    bits 32
    
    ;void my_longjmp(jmp_buf jb, int value);
    my_longjmp:
        mov edx, [esp + 4] ;用 edx 指向 jb
        mov eax, [esp + 8]
        cmp eax, 1
        adc eax, 0 ;确保 eax 值不为零
    
        ;恢复非易失寄存器
        mov ebp, [edx + 0]
        mov ebx, [edx + 4]
        mov edi, [edx + 8]
        mov esi, [edx + 12]
        mov esp, [edx + 16]
    
        add esp, 4 ;弹掉压入的返回值地址
        jmp [edx + 20] ;跳转到目标
        times 4 - ($ - $$ & 3) nop ;使指令长度对齐到 4 的倍数
  • x64 版 _my_longjmp

    bits 64
    
    ;void my_longjmp(jmp_buf jb, int value);
    my_longjmp:
        mov rax, rdx
    
        mov rbx, [rcx + 0x08]
        mov rsp, [rcx + 0x10]
        mov rbp, [rcx + 0x18]
        mov rsi, [rcx + 0x20]
        mov rdi, [rcx + 0x28]
        mov r12, [rcx + 0x30]
        mov r13, [rcx + 0x38]
        mov r14, [rcx + 0x40]
        mov r15, [rcx + 0x48]
        ;rip = [rcx + 0x50]
        ldmxcsr [rcx + 0x58]
        fnclex
        fldcw [rcx + 0x5C]
        movdqa xmm6, [rcx + 0x60]
        movdqa xmm7, [rcx + 0x70]
        movdqa xmm8, [rcx + 0x80]
        movdqa xmm9, [rcx + 0x90]
        movdqa xmm10, [rcx + 0xA0]
        movdqa xmm11, [rcx + 0xB0]
        movdqa xmm12, [rcx + 0xC0]
        movdqa xmm13, [rcx + 0xD0]
        movdqa xmm14, [rcx + 0xE0]
        movdqa xmm15, [rcx + 0xF0]
        jmp [rcx + 0x50]
        times 8 - ($ - $$ & 7) nop ;使指令长度对齐到 8 的倍数
  • x86 版 _jmp_to_new_stack

    bits 32
    
    ;void jmp_to_new_stack(void *stack_buffer, size_t stack_size, void(*function_to_run)(void *userdata), void *userdata, jmp_buf returning, int longjmp_value);
    jmp_to_new_stack:
        mov eax, esp
        mov esp, [eax + 4]
        add esp, [eax + 8] ;设置栈顶
        push dword [eax + 24] ;预先 push 参数给 my_longjmp
        push dword [eax + 20] ;预先 push 参数给 my_longjmp
        push dword [eax + 16] ;传递 `userdata` 参数给回调函数
        call dword [eax + 12] ;调用回调函数
        add esp, 4 ;平栈
        push dword 0xDEADBEEF ;这里本该是返回值地址的,但是我们又不返回。
        jmp my_longjmp
        times 4 - ($ - $$ & 3) nop ;使指令长度对齐到 4 的倍数
    
    %include "x86_my_longjmp.asm"
  • x64 版 _jmp_to_new_stack

    bits 64
    
    ;void jmp_to_new_stack(void *stack_buffer, size_t stack_size, void(*function_to_run)(void *userdata), void *userdata, jmp_buf returning, int longjmp_value);
    jmp_to_new_stack:
        mov rax, rsp
        mov rsp, rcx
        add rsp, rdx ;设置栈顶
        push qword [rax + 48] ;保存 my_longjmp 的参数
        push qword [rax + 40] ;保存 my_longjmp 的参数
        mov rcx, r9 ;传递 `userdata` 参数给回调函数
        sub rsp, 32 ;在x64要给参数预留栈空间
        call r8 ;调用回调函数
        add rsp, 32 ;回收预留的栈空间
        pop rcx ;取回 my_longjmp 的参数
        pop rdx ;取回 my_longjmp 的参数
        jmp my_longjmp
        times 8 - ($ - $$ & 7) nop ;使指令长度对齐到 8 的倍数
    
    %include "x64_my_longjmp.asm"

    就是这些汇编代码,编译出来的 shellcode 就变成了我的 C 源码里面的那些十六进制数组了。
    然后我们还是需要一个结构体来存储我们所需的全部变量的。我写的是这样的:

    typedef struct
    {
    void *new_stack;
    size_t new_stack_size;
    volatile int hack_is_on;
    volatile int is_returned_from_timer;
    WPARAM syscommand_wparam;
    jmp_buf jb_returning;
    jmp_buf jb_reentering;
    jmp_buf jb_exit_hacking;
    }HackWayAntiBlocking;
  • 插入代码到 PollWindowEvents() 的开头部分:

    if (setjmp(w->hack.jb_exit_hacking) == 1)
    { // 此处阻塞式函数退出,从新栈回到这里(原始栈)
        KillTimer(w->Window, 1);
        w->hack.hack_is_on = 0;
    }
    if (w->hack.hack_is_on)
    { // 此处是重入逻辑,也就是要重新跳转回到阻塞函数内部调用我的回调函数的位置上
        if (w->hack.is_returned_from_timer)
            _my_longjmp(w->hack.jb_reentering, 1);
    }
  • 插入代码到 PollWindowEvents() 的结尾部分:

    if (setjmp(w->hack.jb_returning) == 1)
    { // 此处是阻塞式函数内部,我的回调函数被调用后,我跳转到这里假装我的函数是非阻塞的返回。
        return;
    }

    接下来处理消息处理过程 WndProc() 里面的部分

  • 首先我们需要一个在新栈里负责调用阻塞式函数的回调函数,这样写:

    void _on_syscommand_sizemove(WindowsDemoGuts *w)
    { // 就是这个 `DefWindowProcW` 要阻塞式处理鼠标拖拽窗口、调整大小的过程
        DefWindowProcW(w->Window, WM_SYSCOMMAND, w->hack.syscommand_wparam, 0);
    }
  • 然后我们进到消息处理过程 WndProc() 处理 WM_SYSCOMMAND,从它这里进来的默认行为就是让 DefWindowProc() 以阻塞的形式处理

    case WM_SYSCOMMAND:
    switch (GET_SC_WPARAM(wp))
    {
    case SC_MOVE:
    case SC_SIZE:
        w = (void *)GetWindowLongPtrW(hWnd, 0);
        assert(w->hack_is_on == 0);
        w->hack.syscommand_wparam = wp;
        if (!w->hack.new_stack)
        { // 分配新栈空间,就按 64KB 分配,不多不少
            w->hack.new_stack_size = (size_t)1 << 16;
            w->hack.new_stack = _aligned_malloc(w->hack.new_stack_size, 16);
        }
        if (w->hack.new_stack)
        { // 从此处开始跳转进入阻塞式函数,进入新栈。
            w->hack.hack_is_on = 1;
            // 设置定时器。阻塞的时候,定时器依然会生效,我的窗口处理程序可以收到 `WM_TIMER`
            SetTimer(w->Window, 1, 1, NULL);
            _jmp_to_new_stack(w->hack.new_stack, w->hack.new_stack_size, _on_syscommand_sizemove, w, w->hack.jb_exit_hacking, 1);
        }
        else
        { // 新栈内存分配失败,摆烂。
            w->hack.new_stack_size = 0;
        }
        break;
    default:
        return DefWindowProcW(hWnd, msg, wp, lp);
    }
    break;
  • WM_TIMERWM_MOVEWM_SIZE 里面跳出阻塞式函数,使 PollWindowEvents() 可以返回

    case WM_MOVE:
    case WM_SIZING:
    case WM_SIZE:
    case WM_TIMER:
        if (msg != WM_TIMER || wp == 1)
        {
            w = (void *)GetWindowLongPtrW(hWnd, 0);
            if (w->hack.hack_is_on)
            {
                int j = setjmp(w->hack.jb_reentering); // 此处设置重入的入口
                switch (j)
                {
                case 0:
                    // 此处假装返回
                    w->hack.is_returned_from_timer = 1;
                    _my_longjmp(w->hack.jb_returning, 1);
                    break;
                case 1:
                    // 此处是用户再次调用 `PollWindowEvents()` 后重入到这里
                    break;
                default:
                    assert(0);
                }
                w->hack.is_returned_from_timer = 0;
            }
        }
        break;
  • 最后一步:程序退出时的内存释放

    _aligned_free(w->hack.new_stack);
    w->hack.new_stack = 0;
    w->hack.new_stack_size = 0;

    按照以上的方式修改后,就可以实现在拖拽窗口标题栏或者改变窗口大小的时候,不影响你的用户以轮询的方式调用你的 PollWindowEvents()(它会立即返回),并且不影响你的用户流畅地循环更新画面和播放音乐了。

完整代码(github)
完整代码(Gitee)

在我的项目上的应用

我当时在编写一个 POC,展示如何在嵌入式环境下自己拆包 AVI 文件格式(无 malloc()/free()),利用文件读取操作(嵌入式 fatfs + SDMMC)取出其中的 MJPEG 编码的视频流和 PCM 编码的音频流,并进行 JPEG 解码播放和 PCM 播放。我写了一个 Windows 的 Demo,它的解码器是「基于 COM 的 IPicture JPEG 解码」和「基于 waveOutWrite() 的音频输出」。但是嘞,我的 Windows 窗口和消息处理那部分并不是我这程序的主要骨架,而是应当设计为一个模块,提供接口让真正的 AVI 读取器去调用。

  • AVI 读取器调用我的 read()/seek()/tell() 回调函数,读取 AVI。为什么用回调函数呢?因为移植到嵌入式环境的时候,你可以把 fatfs 提供的类似的函数封装一下,传递给 AVI 读取器使用。这样就实现了文件 IO 方面接口可定制性。
  • AVI 读取器调用我的 windows_demo_create_window() 创建一个窗口给他当显示屏用。
    • 嵌入式上接 LCD/OLED 屏幕,用 SPI 或者 RGB 方式驱动屏幕。
  • AVI 读取器调用我的 windows_demo_poll_window_events() 让我处理窗口消息事件等,这个函数不能阻塞,因为解码器会进入播放循环,在这个播放循环里,它会调用我的这个函数来防止我的窗口变成一个不响应窗口消息的假死窗口。
    • 嵌入式上每进行一个循环,调用一个回调函数,这个回调函数里可以通过读 GPIO 来响应物理按键等用户操作。
  • AVI 读取器调用我的 windows_demo_show_video_frame() 完成 JPEG 的解码。
    • 嵌入式上则使用 JPEG 硬解外设进行解码,要么就是使用 libjpeg 进行软解。
  • AVI 读取器调用我的 windows_demo_play_audio_packet() 完成 PCM 音频播放。
    • 嵌入式上使用 DAC 或者 IIS 进行音频播放,配合 DMA 双缓冲不断提供音频数据。
  • AVI 读取器调用我的 windows_demo_destroy_window() 做销毁工作。
    • 嵌入式上则是停止播放,如果用 fatfs 打开了文件那就关闭文件,然后显示用户菜单等玩意儿。

开发 AVI 播放器的过程总体上来说挺轻松的,AVI 文件结构简单,你只要弄懂了它用 Chunk 组织数据块的逻辑(Chunk 可以嵌套 Chunk,有的 Chunk 的数据部分的开头会使用 FourCC 来注明这个 Chunk 里面装的是啥),再利用 Winhex 等 16 进制文件查看器去检查一个 AVI 文件是不是真的符合文档所说的,你就能编写出你自己的 AVI 读取器和 AVI 写入器。

当我的视频开始流畅地播放、悦耳的音乐在耳机里响起的时候我还是很开心的,但是当我想要把窗口拖动到最中间,并且调整大小的时候,他就阻塞,然后我就音画不同步(后来我通过跳帧逻辑解决了音画不同步的问题)。这我就不开心了。虽然有别的方法解决,但是我实在是不太能接受这种阻塞的效果。我就整出了这套黑科技。

AVI 文件格式参考



微软的 longjmp() 轮子里面的内容还是有很多幽默的地方的。
longjmp.png
回复

使用道具 举报

发表于 4 天前 | 显示全部楼层
太折腾了

我如果需要这种效果的话,大概率会把窗体设置为[不可变更大小],然后自己实现一套[鼠标移动到边框上改变指针为对应的调整指针]的逻辑,以及[在边框上拖动鼠标时改变窗体大小]的逻辑....

并且,我会让AI给我写代码,我调试一下就好
回复 赞! 靠!

使用道具 举报

发表于 4 天前 | 显示全部楼层
微软的那代码是因为prologue和epilogue问题,代码自动审查会给过,这种情况我习惯用
option prologue:none
option epilogue:none
关闭掉自动的栈恢复
但是汇编用
push [edx+20]
ret
自己让代码包含ret 让代码看起来像一个完整的函数
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 4 天前 | 显示全部楼层
嗷嗷叫的老马 发表于 2025-6-11 14:00
太折腾了

我如果需要这种效果的话,大概率会把窗体设置为[不可变更大小],然后自己实现一套[鼠标移动到边框 ...

这个年代有谁说得清自己的代码的「AI 含量」有多少呢?我其实也用了 AI,不过手搓汇编那块儿只能靠自己。
回复 赞! 靠!

使用道具 举报

发表于 3 天前 | 显示全部楼层
嗷嗷叫的老马 发表于 2025-6-11 14:00
太折腾了

我如果需要这种效果的话,大概率会把窗体设置为[不可变更大小],然后自己实现一套[鼠标移动到边框 ...

自己实现的标题栏拖动和Windows原生的效果差太多了,而且有阻塞情况的又不只是标题栏,还有右键弹出菜单和所有的非客户按钮。我推荐的唯一解就是多线程(视频渲染或游戏渲染都放到子线程去)。
回复 赞! 靠!

使用道具 举报

发表于 3 天前 | 显示全部楼层
还好我这工控狗不需要考虑这些
回复 赞! 靠!

使用道具 举报

发表于 前天 20:05 | 显示全部楼层
YY菌 发表于 2025-6-12 11:43
自己实现的标题栏拖动和Windows原生的效果差太多了,而且有阻塞情况的又不只是标题栏,还有右键弹出菜单 ...

游戏里一般是在只有阻塞的消息里单独开线程处理渲染,而且也是类似的垂直同步选项开启了之后才处理,否则也是不处理
回复 赞! 靠!

使用道具 举报

发表于 前天 20:50 | 显示全部楼层
AyalaRs 发表于 2025-6-13 20:05
游戏里一般是在只有阻塞的消息里单独开线程处理渲染,而且也是类似的垂直同步选项开启了之后才处理,否则 ...

拖动标题栏不就是阻塞的消息吗?反正我选择直接在单独一个子线程渲染,先天解决一切问题。
回复 赞! 靠!

使用道具 举报

发表于 前天 22:04 | 显示全部楼层
YY菌 发表于 2025-6-13 20:50
拖动标题栏不就是阻塞的消息吗?反正我选择直接在单独一个子线程渲染,先天解决一切问题。 ...

单独一个子线程渲染还要解决和ui线程通信问题,又是一堆事
回复 赞! 靠!

使用道具 举报

发表于 昨天 17:34 | 显示全部楼层
AyalaRs 发表于 2025-6-13 22:04
单独一个子线程渲染还要解决和ui线程通信问题,又是一堆事

谁说的?游戏渲染线程跟UI线程通信来搞毛啊?游戏渲染线程只管渲染游戏画面就OJBK了。
回复 赞! 靠!

使用道具 举报

发表于 昨天 22:38 | 显示全部楼层
YY菌 发表于 2025-6-14 17:34
谁说的?游戏渲染线程跟UI线程通信来搞毛啊?游戏渲染线程只管渲染游戏画面就OJBK了。 ...

你认真的么,就算最简的渲染线程也和ui线程有共享数据区来保持信息一致性,或者依赖gpu的信号量,又不是放动画,渲染也是基于交互回馈的
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2025-6-15 03:13 , Processed in 0.050671 second(s), 30 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表