• RuntimesBugs
  • Serious bug in Spine-C not fixed for 30 days.

We are encountering critical issues with the spine-c runtime in version 4.2, which are blocking our production use. Our Spine-based rendering system and product are built directly on top of the official spine-c implementation.

Problem statement

  1. darktint-RGB-Timeline causes fatal errors or incorrect behavior
    • Binary file case: Parsing a spine binary that includes darktint-RGB-Timeline results in a fatal crash due to invalid memory access.
    • JSON file case: Parsing a JSON file containing darktint-RGB-Timeline does not crash, but produces incorrect rendering results — the timeline appears to be parsed incorrectly or ignored.

  2. spSkeletonBinary_create fails to honor custom scale factor
    • When using spSkeletonBinary_create(...) with a custom scale value (e.g., for resolution scaling),
    • the animation behaves abnormally:
    • Attachment positions and sizes are not scaled.
    • The referenceScale does not reflect the custom scale (unlike spSkeletonJson_create(...), which works as expected).

Pull Request of below 2 will fix the issue, but no reply or activity is ongoing over 30 days.

  • EsotericSoftware/spine-runtimes2864
  • EsotericSoftware/spine-runtimes2863

    Runtime information

    • Spine Runtime: spine-c v4.2
    • Platform: Cross-platform C/C++ engine
    • Files tested: .spine → exported to .json and .skel with dark tint timelines (RGB2 not RGBA2) and scale factors
    • Expected behavior: spSkeletonBinary_create() should honor the scale factor; timelines should parse and animate correctly.

Please review the above PRs or let us know if there’s any blocker preventing these from being merged. These issues are critical to the integrity of the runtime in production environments, and we are happy to provide test cases or further assistance.

Thanks so much for your time and the amazing work on Spine!

Related Discussions
...

Sorry for the delay! We'll get this sorted ASAP!

There was a mistake from my side too.
I realized it after the reply. Sorry for making confuse.

I extended the PR with the issue EsotericSoftware/spine-runtimes2879

Thanks for reporting. This has been fixed in the 4.2 branch.

Please note that in Spine 4.3, we are removing spine-c in favor of spine-cpp-lite, a C wrapper around spine-cpp. This consolidation to a single implementation for native runtimes should help improve the native runtimes considerably.

  • pbk a répondu à ça.

    Mario
    I appreciate your fast response. Thank you.
    I understand the decsion, of dropping spine-c and using spine-cpp-lite. But that's a bad news for me to hear.
    I did try using cpp-lite before using spine-c, but it lacks of essential features and bugs that I can not fix without affecting other cpp supported platform.

    Bug for example, iOS pma issue(this needs to touch skeletonRenderer.cpp), blend mode issue

    Lacking feature: TrackEntry specific callback for example, hidden interfaces

    In my experience from several contribution to spine spine-ios and spine-cpp-lite, making runtime from spine-c rather than reusing existing is much easier to handle those issue.

    Anyway, I hope spine-ios, spine-cpp-lite could handle these in near future. I really appreciate this awesome product.

    7 jours plus tard

    The new spine-cpp-lite (called spine-c going forward) is a complete rewrite. In fact, it is auto-generated from spine-cpp and include access to everything the public spine-cpp API exposes, except for listeners. If you can show me what exactly you need wrt to listeners, we can work that into the new spine-c.

    The iOS PMA bug will also be fixed in the 4.3 release.

    You can check out a the new spine-c here:
    EsotericSoftware/spine-runtimestree/4.3-beta/spine-c

    It is currently a work in progress as we fix some final bugs. But the API surface will stay like this!

    • pbk a répondu à ça.

      Mario

      Great to hear that!

      I tried the project, I get the linker error from CurveTimeline2::getCurveValue which is defined but no implementation.
      CurveTimeline2::getCurveValue is defined only in header and never implemented in even 4.2
      Even Spine-api reference doesn't define this "https://ko.esotericsoftware.com/spine-api-reference#CurveTimeline2-Methods" even in "libgdx" version does not defines it.

      Talking about eventListener, I found a way transfering clang block syntax to the std::function
      https://clang.llvm.org/docs/BlockLanguageSpec.html

      clang block syntax requires clang compiler but supports capturing closure context syntax in objective-c, c.

      // this is block_helper.mm file which compiles as objective-c++,  using objctive-c++ compiler with objc_arc
      // otherwise `block` needs to be manually reference counted using Block_copy, Block_release
      // below condition is always true in standard apple platform sdk (macOS, watchOS, visionOS, iOS, tvOS)
      // without __has_feature(objc_arc) block needs to be managed using Block_copy, Block_release
      #if __OBJC__ && __has_feature(objc_arc) && __BLOCKS__ 
      #include <Block.h>
      typedef void (^SpineAnimationCallbackBlock)(spine_animation_state _Nonnull state, spine_event_type type, spine_track_entry _Nonnull entry, spine_event _Nullable event);
      
      void spine_track_entry_set_block(spine_track_entry _Nonnull entry, SpineAnimationCallbackBlock _Nullable block) {
          if (!entry) {
              return;
          }
          auto self = reinterpret_cast<spine::TrackEntry *>(entry);
          if (!block) {
              self->setListener(0);
          } else {
              self->setListener([block](spine::AnimationState* state, spine::EventType type, spine::TrackEntry *trackEntry, spine::Event *event) {
          
                  @autoreleasepool {
                      block(
                            reinterpret_cast<spine_animation_state>(state),
                            static_cast<spine_event_type>(type),
                            reinterpret_cast<spine_track_entry>(trackEntry),
                            reinterpret_cast<spine_event>(event)
                            );
                  }
              });
          }
      }
      
      void spine_animation_state_set_block(spine_animation_state _Nonnull state, SpineAnimationCallbackBlock _Nullable block) {
          if (!state) {
              return;
          }
          auto self = reinterpret_cast<spine::AnimationState *>(state);
          if (!block) {
              self->setListener(0);
          } else {
              self->setListener([block](spine::AnimationState* state, spine::EventType type, spine::TrackEntry *trackEntry, spine::Event *event) {
                  @autoreleasepool {
                      block(
                            reinterpret_cast<spine_animation_state>(state),
                            static_cast<spine_event_type>(type),
                            reinterpret_cast<spine_track_entry>(trackEntry),
                            reinterpret_cast<spine_event>(event)
                            );
                  }
              });
          }
      }
      
      #endif

      Also is there any plan to create C wrapped TextureLoader interface.
      for c-header third-party language users to implement their own textureLoader?
      Ability to create custom TextureLoader, is crucial to load texture in non standard way.
      (deferred texture loading, external texture loading, non png resources: avif/heic and metal texture asset pack)

      Below sample code is inspired by CoreFoundation CFAllocator
      https://developer.apple.com/documentation/corefoundation/cfallocatorcreate(_:_:)

      typedef struct {
          void* _Nullable  (* _Nullable initialize)(void* _Nullable input);
          void (* _Nullable deinitialize)(void* _Nullable loader);
          void (* _Nullable load)(spine_atlas_page _Nonnull  page, const char* _Nonnull path, void* _Nullable context);
          void (* _Nullable unload)(void* _Nullable texture, void* _Nullable context);
      } SpineTextureLoaderVtable;
      
      
      class SpineCTextureLoader : public spine::TextureLoader {
        
      public:
          const SpineTextureLoaderVtable* c_table;
          void* context;
          
          void load(spine::AtlasPage &page, const spine::String &path) override {
              if (c_table && c_table->load) {
                  auto c_page = reinterpret_cast<spine_atlas_page>(&page);
                  c_table->load(c_page, path.buffer(), context);
              }
          }
          
          void unload(void *texture) override {
              if (c_table && c_table->unload) {
                  c_table->unload(texture, context);
              }
          }
          
          ~SpineCTextureLoader() override {
              if (c_table && c_table->deinitialize) {
                  c_table->deinitialize(context);
              }
          }
          
          
          SpineCTextureLoader(const SpineTextureLoaderVtable* table, void* input): c_table(table), context(0) {
              if (c_table && c_table->initialize) {
                  context = c_table->initialize(input);
              }
          }
          
          
      };
      
      #define SIZEOF_TEXTURELOADER_CWRAPPER 24
      
      typedef struct {
          uint8_t cpp_object[SIZEOF_TEXTURELOADER_CWRAPPER];
          SpineTextureLoaderVtable loader; 
      } MyTextureLoader1;
      
      typedef struct {
          uint8_t cpp_object[SIZEOF_TEXTURELOADER_CWRAPPER];
      } MyTextureLoader2;
      
      
      
      static_assert(aligned_sizeof<SpineCTextureLoader>() <= SIZEOF_TEXTURELOADER_CWRAPPER,
                    "SIZEOF_TEXTURELOADER_CWRAPPER too small!");
      
      void spine_initialize_texture_loader(MyTextureLoader1* storage, void *input) {
          if (storage) {
              std::memset(storage, 0, sizeof(storage->cpp_object));
              auto loader = new (storage->cpp_object) SpineCTextureLoader(&storage->loader, input);
          }
      }
      
      
      void spine_deinitialize_texture_loader(MyTextureLoader1* storage) {
          if (storage) {
              spine::TextureLoader* loader = reinterpret_cast<spine::TextureLoader*>(storage);
              loader->~TextureLoader();
              std::memset(storage, 0, sizeof(storage->cpp_object));
          }
      }
      
      void spine_initialize_texture_loader_2(MyTextureLoader2* storage, const SpineTextureLoaderVtable* table, void *input) {
          if (storage) {
              std::memset(storage, 0, sizeof(storage->cpp_object));
              auto loader = new (storage->cpp_object) SpineCTextureLoader(table, input);
          }
      }
      
      void spine_deinitialize_texture_loader_2(MyTextureLoader2* storage) {
          if (storage) {
              spine::TextureLoader* loader = reinterpret_cast<spine::TextureLoader*>(storage);
              loader->~TextureLoader();
              std::memset(storage, 0, sizeof(storage->cpp_object));
          }
      }

      Yeah, you can pass callbacks to spine_atlas_load_callback:
      EsotericSoftware/spine-runtimesblob/4.3-beta/spine-c/src/extensions.h#L75

      Some runtimes actually do not supply any texture loader callbacks but instead walk through the parsed atlas pages after spine_atlas_load, figure out the image paths, then load them however they want.

      E.g. that's what the Dart/Flutter runtime does:
      EsotericSoftware/spine-runtimesblob/4.3-beta/spine-flutter/lib/spine_flutter.dart#L124

      It's Dart that calls directly into the spine-c API through FFI. Note that I haven't updated spine-flutter to use the new, generated spine-c API. But the principles hold.