Как и в любой задаче, здесь есть несколько вариантов решения. Каждый вариант обладает своими достоинствами и характеризуется степенью сложности реализации. На вскидку, мне кажется, что следующий вариант будет лучшем в вашем случае:
Отдельный компонент-контрол TSnowFall
Назовем его условно TSnowfall (снегопад). Он будет рисовать на себе падающие снежинки. Мы располагаем его в корне формы и растягиваем на весь экран Align = alContent, так же всегда располагаем его поверх всех остальных контролов (BringToFront) и запретим ему перехватывать события мыши (HitTest = False), чтобы он не нарушал логику работы всех остальных контролов. Затем в перекрытом методе TControl.Paint выполняем отрисовку снежинок с текущими позициями. А в созданном вручную таймере по каждому тику меняем текущее положение всех снежинок и вызываем метод перерисовки TControl.Repaint.
На мой взгляд этот вариант хорош:
Полное отделение логики от кода формы. То есть код формы будет проще за счет убирания всего кода работы с отрисовкой снежинок в отдельный юнит.
Легкое расположение занавеса снежинок в любом месте. С возможностью привязки к любому контролу и без.
Повторное использование в любом другом месте.
Постусловие
Каждая форма FireMonkey имеет одну канву. Поэтому все контролы по сути рисуются на ней, в отличии от VCL, где каждый контрол с оконным хендлом имеет свой графический контекст -> канву.
Когда форма получает запрос на отрисовку, она по шаблону цепочка ответственности делегирует всем контролам по дереву запрос на отрисовку.
В каждом контроле происходит подготовка общей канвы под отрисовку нужного контрола, который в текущий момент отрисовывается. То есть задается матрица трансформации, чтобы контрол работал в своей локальной системе координат, а не в абсолютной СК формы. И тд. Поэтому теоретически вы можете рисовать поверх любых контролов в TForm.OnPaint, но это правильно. Лучше использовать мой предложенный подход, описанный выше.