Предистория: Я решил-таки сделать себе в программе рисования удобно, как было на zx-spectrum. То есть - программа рисования рисует себе в цикле, когда нужно точечки на экран ставит, точечки появляются на экране и остаются там пока программа не перезапишет. Никаких спецусилий, никаких event loop'ов и т.д.
Я хотел иметь то же самое в условиях оконного отображения. Оконное отображение - это event loop, надо реагировать на кнопочки и команды выхода. Если мы в цикле 10с считаем Солнышко Лиссажу, то мы 10с не реагируем на кнопки (aka зависли). Классические подходы: резать задачу на кусочки и перемежать их с ответом на event loop. Плюсы: относительно просто. Минусы - не ясно где резать, можно промахнуться с отправкой отрисованного и получить лаг на экране. Второй подход: соседний тред, который шлёт мелкие сообщения об обновлениях, основной тред рисует их на экране и отвечает за кнопки. Плюсы: если сообщения мелкие, практически гарантировано можно успеть ответить на event. Минусы: безумные накладные расходы на сообщения, проблема "очереди" (либо блокирующая, либо память течёт). В целом, если точки генерируются достаточно быстро, то снова та же проблема "когда прекратить принимать сообщения и отправлять кадр на рендеринг"?
Моя идея: shared область памяти, в которую рисующий (считающий) тред пишет непрерывно (как хочет), а отображающий заглядывает для обновления текстуры. Схватил текстуру, отобразил в буффер, дальше библиотека SDL (да и любая другая) с режимом vsync вернёт управление как только будет отрисован кадр на экране. Ляпота, да?
Только была пробелма: Rust не любит двойной доступ к памяти. Это тот самый danger quadrant, где конкуретный read и write.
В процессе я нырнул глубоко в unsafe, прочитал много про pointer'ы, в целом стал лучше понимать rust.
итог: код без атомиков (я себе таки его написал прямо сейчас) не работает от слова вообще. Один тред пишет, второй наслаждается кешем процессора.
Код с атомиками я сначала написал ужасно, с двойным указателем в один и тот же slice, отдаваемый в разные треды. unsafe, unsound and dangerous. Но он работал с офигенной скоростью в 13.5 ГБ/с (в режиме максимального congestion, когда один тред непрерывно меняет ячейку, а второй непрерывно её читает).
Мне сказали "переходи на arc". Для меня было открытием, что atomic позволяет сделать store для значения на non-mut ref, но я пошёл и написал. Стало порядка 9ГБ/с.
И тут мне сказали про Arc<[AtomicU32]>. Это было безумие! У меня скорость стала ещё выше - 17.5ГБ/с. Я подозреваю, что это подозрительно близко к скорости моей памяти (25.5ГБ/с в один канал), и, возможно, ограничена скоростью IPI (который как раз кеши процессорам и инвалидирует).
При этом в коде 0 unsafe!!! Я писаю кипятком.
А ещё я проверил насколько проверка индексов всё тормозит. Ответ: мало тормозит. около 0.2с (с 9.9s на 10 прогонов полного цикла по u32 до 9.7s), и оно того не стоит.
Фух. Сейчас я пойду приделывать это всё в свой основной проект. Подумав головой, я хочу сделать хитрое - засунуть рисовалку в дополнительный тред, а в основном оставить только код рисования.
Чтобы было так:
fn main(){
let mut screen = Screen::new();
for i in foo(){
screen.put_pixel(x, y);
}
screen.done();
}
А всякая SDL'ная фигня была в этом самом screen::new().
Это будет моя спектрумовская мечта. Берёшь и рисуешь, no thinking required. Я думаю, она много кому понравится как библиотека, хотя рисковать делать библиотеку пока я не готов (вероятнее всего, задумаюсь, по результатам успеха).
PS Вот proof-of-concept: https://github.com/amarao/rust_dual/blob/arc/src/main.rs