Mar. 14th, 2021

amarao: (Default)
Если описать странную проблему, то (иногда) можно получить in-depth ответ. У меня моя последняя итерация демки мерцала, и я совершенно не понимал почему. Но, объяснили - два буффера, которые never converge если к этому не прикладывать специальных усилий.

https://stackoverflow.com/questions/66617330/why-there-is-a-flickering-of-old-image-if-i-change-vew-port-for-canvas-before-ca
amarao: (Default)
Одна из проблем, которая меня очень смущает, это проблема синхронизации времени и алгоритма.

В целом, скорость процессора - это такая внешняя сущность, не относящаяся к алгоритму (и коду). Один и тот же код на разных процессорах будет выполняться за совершенно разное время (банальность, да, но важная).

Вывод на экран, который мы можем считать подобием silky smooth, требует выполнять операции за фиксированное время. Промахнулись? Остались без кадра. Визуально это выглядит как рывок или тормоза.

Т.е. у вас в алгоритме есть некая внешная сущность, которая для алгоритма почти подобна рандому - "сколько времени прошло?". И эта сущность нам говорит, сколько мы можем сделать (в рамках внутренних метрик алгоритма, допустим, "операций").

Нам надо сделать 2 миллиона операций. Это константа runtime'а. А вот сколько кадров это займёт - это некая случайная величина, которую мы не контролируем.

Можно порезать операции на кусочки и выполнять их в цикле, проверяя, "не пора ли заканчивать" (нормально, если обсчёт займёт больше одного кадра, важно не "протупить" в обработке текущего, а продолжить можно будет для следующего). Но на какие кусочки резать? Эмпирика? Микробенчмарк прямо в коде? Автоподстройка по факту пропуска кадра?

Во время моих экспериментов с пистоном я пробовал все их три. Увы, работали плохо.

Была даже четвёртая альтернатива - считать в соседнем треде, и быть готовым "ответить" в любой момент времени накопившимися изменениями. Это, по сути, мало отличается от "отложить рендеринг на следующий фрейм", потому что запросы нужно проверять в event loop'е, и нет никакой разницы, в одном это треде, или в двух. Всё равно работу нужно резать на куски, а какого размера куски не понятно.

Пятая альтернатива пока что звучит интересно: иметь shared buffer, в который один тред пишет, а второй читает. Один тред всё время пишет, второй читает когда нужно. В силу специфики проблемы (попкисельный рендеринг) нет разницы между "чтением в середине записи" и "откладыванием половины работы на следующий тик".

Я уже себя ругаю (ещё раз) за то, что я это делаю, но, допустим, я попробую это. С алгоритмической точки зрения это наиболее разумная идея (мы считаем, не думая про читателя, т.е. просто считаем).

Заодно, познакомлюсь с тем, как шарить mut-память в rust'е между тредами. (Да, я знаю, что это instant-UB, но уж очень соблазнительная идея).
amarao: (Default)
Написал MR в rust-sdl2 с докой про Blend mode. Получил тонны опыта (cargo fmt, cargo doc). С меня - почти копипаст с вики SDL. Мне - краткий курс rust lore на 10 минут. Очень, очень ценно.
amarao: (Default)
pub struct Screen{
    bytes: Vec<std::sync::atomic::AtomicU32>,
    width: u32,
    height: u32
}

impl Screen {
    pub fn copy_to_buffer<T>(&self, target: &mut [T]) -> Result<(), &'static str>
    where T: bytemuck::Pod{
        if self.bytes.len() != target.len(){
            return Err("Size of target should be the same as source.")
        }
        let target_u32: &mut [u32] = bytemuck::cast_slice_mut(target);
        for idx in 0..self.bytes.len(){
            target_u32[idx] = self.bytes[idx].load(Relaxed);
        }
        Ok(())
    }
}

Так сказать, "а что, так можно было"?

Если мне сейчас удастся малой кровью сделать его мутабельным (я верю в тебя, атомик) и пропихнуть между тредами, то это же просто праздник какой-то. У меня будет шаренная между тредами память с Relaxed моделью, в которой в одном треде я мирно делаю copy_to_buffer (для нужд рендеринга в текстуру), а в другом я в него делаю set_pixel/get_pixel, которые работают со скоростью mov для u32.

Что мне нужен атомик, меня уговорили на форуме, пригрозив, что без атомика компилятор может просто соптимизировать повторный доступ к непоменявшимся (с его точки зрения) данным.

Бенчмарк показал, что запись в атомик в Relaxed занимает столько же времени, как и в обычный u32.

amarao: (Default)

Я пока не до конца уверен, что я в правильном направлени иду, но вот какая у меня идея:

Мне нужно в новый тред отдавать ссылки только со 'static, а я хочу туда отдать (из метода структуры) ссылку на кусок "себя". Что, очевидно, нарушает 'static, потому что если "себя" сделают drop() по выходу из блока/функции, будет danging pointer, а весь раст строился вокруг того, чтобы такого не было. И нужно именно так сделать.

Задача: сохранить инвариант. Т.е. сделать так, чтобы при drop() себя, отданная ранее в соседний тред ссылка была бы жива. Это традиционная задача AI tetris (https://www.youtube.com/embed/xOCurBYI_gY?start=910&feature=oembed&enablejsapi=1), и она прекрасно решается с помощью bottom type.

impl Drop for Screen{
    fn drop(&mut self){
        if let Some(..) = self.thread {
            println!("Attempt to drop Screen. Looping forever.");
            loop{}
        }
    }
}

Такой подход гарантирует: а) что ссылка жива до конца программы. б) что структуру можно создавать с не 'static lifetime, потому что есть drop().

В самой программе нужно, конечно, выходить не через return:

        match event {
            Event::Quit { .. }
            | Event::KeyDown {
                keycode: Some(Keycode::Escape),
                ..
            } => std::process::exit(0),
            _ => {}
        }

Это была простая часть. А теперь сложная - как бы мне сделать mut slice и slice на один и тот же набор данных, чтобы мне за это не особо много влетело?

stay tuned.

Profile

amarao: (Default)
amarao

August 2025

S M T W T F S
     12
345 6789
10111213141516
17181920212223
24252627282930
31      

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Aug. 31st, 2025 03:57 am
Powered by Dreamwidth Studios